I think it’s good to confess the errors you made when you created bugs in your software. It makes you a better programmer to write out the process of creating and fixing the bug, and might help someone else at the same time. This is one of those confessions.
Castalia often has a need to determine whether two filenames are the same. For example, the code that maintains internal data about the code you’re working on very frequently checks to see if you’re still working on the same file you were last time. It does this by comparing the name of the file you’re working on with the name of the file you were working on last time it checked. If they’re the same, it’s the same file. If they’re different, then you’re working on a different file.
Easy enough, right? Filenames are just strings, so it’s a simple string comparison.
Of course, there has to be a complication. The complication is that the Delphi ToolsAPI doesn’t always give you the filename in the same format. It’s usually the full path to the file, but occasionally, it will be just the extracted filename.
For example, when Castalia gets the name of the current file from the ToolsAPI, it will probably get a string like ‘c:\users\jacob\documents\project1\MainForm.pas’, but it might just get ‘MainForm.pas.’
So, I wrote an IsSameFile routine that would take two filenames as strings, and compare them in both cases (full path, or just filename), to determine if they are, in fact, the same file:
function IsSameFile(N1, N2: string): Boolean; begin Result := CompareText(N1, N2) = 0; //Usually this works, for full paths if not Result then //If it's false, maybe we have a simple filename. Check that Result := CompareText(ExtractFileName(N1), ExtractFileName(N2)) = 0; end;
This worked great, until someone came across an edge case that proved to be a bug in this function.
See if you can spot it. I’ll wait…
OK, did you find it?
Here’s the problem: What if the “short” filenames are the same, but are, in fact, different files? For example:
N1 is ‘c:\users\jacob\documents\project1\MainForm.pas’
N2 is ‘d:\projects\project2\MainForm.pas’
The function above will first try to compare N1 and N2, and then when that’s false, will compare the “short” file names, which happen to match, even though it’s painfully obvious that these are not, in fact, the same file.
There are two fundamental errors in logic here. One is thinking there only two cases: 2 full paths, or 2 “short” names. The second is thinking that the second case is always triggered by the first case being false.
First, realizing that there are actually three distinct cases for comparing the two filenames:
- Compare two filenames with full paths
- Compare two “short” filenames, with no path
- Compare one “short” filename with one full path
Second, these cases need to be detected independently. Just because the first case is false doesn’t mean that wasn’t the right case. There needs to be a mechanism to determine which case to use, and then compare the appropriate strings.
Here’s the fixed IsSameFile function:
function IsSameFile(N1, N2: string): Boolean; var S1, S2: string; //Short filenames begin S1 := ExtractFileName(N1); S2 := ExtractFileName(N2); if (S1 <> N1) and (S2 <> N2) then begin //2 full path Result := CompareText(N1, N2) = 0; end else if (S1 = N1) and (S2 = N2) then begin //2 filename only Result := CompareText(N1, N2) = 0; end else begin //1 short, 1 path Result := CompareText(S1, S2) = 0; end; end;
This handles all three cases correctly, and more importantly, determines which case to use the right way.
This addressed a bug where Castalia’s navigation toolbar would get confused if you opened two files with the same filename, either at the same time, or in sequence (for example, working on MainForm.pas in one project, then closing that project and immediately opening MainForm.pas from another project). That bug is fixed, thanks to the fixed IsSameFile() function, as of Castalia 2014.5.