Skip to content

Commit 2a5fb7b

Browse files
committed
First stab at implementing an indexer
1 parent 7da5e77 commit 2a5fb7b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+9405
-74
lines changed

.github/workflows/test.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,3 @@ jobs:
3030
with:
3131
dotnet-version: 6.0.x
3232
- run: dotnet test
33-

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.DS_Store
2+
.idea
13
## Ignore Visual Studio temporary files, build results, and
24
## files generated by popular Visual Studio add-ons.
35
##
@@ -348,3 +350,4 @@ MigrationBackup/
348350

349351
# Ionide (cross platform F# VS Code tools) working folder
350352
.ionide/
353+

NOTICE

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# tcz717/LsifDotnet
2+
3+
scip-dotnet contains contains parts which are derived from the
4+
tcz717/LsifDotnet project. The original license is Apache 2 and can be
5+
found here: https://github.com/tcz717/LsifDotnet/blob/main/LICENSE
6+
7+
The parts that are derived from LsifDotnet are mostly related to parsing
8+
command-line options and loading the MSBuild Workspace, not the indexing logic
9+
itself.
10+

ScipDotnet.Tests/Position.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Scip;
2+
3+
namespace ScipDotnet.Tests;
4+
5+
public record Position(int Line, int Character) : IComparable<Position>
6+
{
7+
public int CompareTo(Position? other)
8+
{
9+
if (other == null)
10+
{
11+
return 1;
12+
}
13+
14+
if (other.Line != Line)
15+
{
16+
return Line - other.Line;
17+
}
18+
19+
return Character - other.Character;
20+
}
21+
22+
}

ScipDotnet.Tests/Range.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using Scip;
2+
3+
namespace ScipDotnet.Tests;
4+
5+
public record Range(Position Start, Position End) : IComparable<Range>
6+
{
7+
public int CompareTo(Range? other)
8+
{
9+
if (other == null)
10+
{
11+
return 1;
12+
}
13+
14+
var byStart = Start.CompareTo(other.Start);
15+
if (byStart != 0)
16+
{
17+
return byStart;
18+
}
19+
20+
return End.CompareTo(other.End);
21+
}
22+
23+
public static Range FromOccurrence(Occurrence occ)
24+
{
25+
if (occ.Range.Count == 3)
26+
return new Range(new Position(occ.Range[0], occ.Range[1]), new Position(occ.Range[0], occ.Range[2]));
27+
28+
if (occ.Range.Count == 4)
29+
return new Range(new Position(occ.Range[0], occ.Range[1]), new Position(occ.Range[2], occ.Range[3]));
30+
31+
throw new ArgumentException($"occurrence.range must have length 3 or length 4, obtained {occ.Range}");
32+
}
33+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="DiffPlex" Version="1.7.1" />
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
13+
<PackageReference Include="NUnit" Version="3.13.3" />
14+
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
15+
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
16+
<PackageReference Include="coverlet.collector" Version="3.1.2" />
17+
<PackageReference Include="TextCopy" Version="6.2.0" />
18+
<PackageReference Include="Verify.NUnit" Version="19.3.0" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<ProjectReference Include="..\ScipDotnet\ScipDotnet.csproj" />
23+
</ItemGroup>
24+
25+
</Project>

ScipDotnet.Tests/SnapshotTests.cs

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
using System.Diagnostics;
2+
using System.Text;
3+
using DiffPlex.DiffBuilder;
4+
using DiffPlex.DiffBuilder.Model;
5+
using Scip;
6+
using Index = Scip.Index;
7+
8+
namespace ScipDotnet.Tests;
9+
10+
[TestFixture]
11+
public class SnapshotTests
12+
{
13+
[Test, TestCaseSource(nameof(ListSnapshotInputDirectories))]
14+
public void Snapshot(string inputDirectory)
15+
{
16+
var indexFile = IndexDirectory(inputDirectory);
17+
var indexBytes = File.ReadAllBytes(indexFile);
18+
var index = Index.Parser.ParseFrom(indexBytes);
19+
var snapshots = index.Documents.ToDictionary(document => document.RelativePath,
20+
document => FormatDocument(index, document));
21+
var outputDirectory =
22+
Path.GetFullPath(Path.Join(inputDirectory, "../../", "output", Path.GetFileName(inputDirectory)));
23+
var isUpdateSnapshots = Environment.GetEnvironmentVariable("SCIP_UPDATE_SNAPSHOTS") != null;
24+
if (isUpdateSnapshots)
25+
{
26+
if (Directory.Exists(outputDirectory))
27+
{
28+
Directory.Delete(outputDirectory, true);
29+
}
30+
31+
foreach (var (relativePath, snapshot) in snapshots)
32+
{
33+
var documentPath = Path.Join(outputDirectory, relativePath);
34+
var directory = Path.GetDirectoryName(documentPath);
35+
if (directory == null) continue;
36+
Directory.CreateDirectory(directory);
37+
File.WriteAllText(documentPath, snapshot);
38+
}
39+
}
40+
else
41+
{
42+
var files = new List<string>();
43+
RecursivelyListFiles(outputDirectory, files);
44+
Assert.Multiple(() =>
45+
{
46+
foreach (var file in files)
47+
{
48+
var relativePath = Path.GetRelativePath(outputDirectory, file);
49+
var obtained = snapshots.GetValueOrDefault(relativePath, "");
50+
var expected = File.ReadAllText(file);
51+
var diff = DiffStrings(obtained, expected);
52+
if (diff.Length > 0)
53+
{
54+
Assert.Fail("(+ expected, - obtained). To update the expected output to match the obtained behavior, run: " +
55+
"SCIP_UPDATE_SNAPSHOTS=true dotnet test\n\n" + diff, file);
56+
}
57+
}
58+
59+
var filesSet = new HashSet<string>(files);
60+
foreach (var (relativePath, _) in snapshots)
61+
{
62+
var outputPath = Path.Join(outputDirectory, relativePath);
63+
if (filesSet.Contains(outputPath))
64+
{
65+
continue;
66+
}
67+
68+
Assert.Fail(
69+
$"relative path '{relativePath}' missing an output file. To fix this problem, run the following command: SCIP_UPDATE_SNAPSHOTS=true dotnet test");
70+
}
71+
});
72+
}
73+
}
74+
75+
private static string DiffStrings(string obtained, string expected)
76+
{
77+
var diff = InlineDiffBuilder.Diff(expected, obtained);
78+
var sb = new StringBuilder();
79+
for (var i = 0; i < diff.Lines.Count; i++)
80+
{
81+
var line = diff.Lines[i];
82+
if (line.Type == ChangeType.Unchanged)
83+
{
84+
continue;
85+
}
86+
87+
if (i > 0 && diff.Lines[i - 1].Type == ChangeType.Unchanged)
88+
{
89+
sb.Append(" ").AppendLine(diff.Lines[i - 1].Text);
90+
}
91+
92+
switch (line.Type)
93+
{
94+
case ChangeType.Inserted:
95+
sb.Append("+ ");
96+
break;
97+
case ChangeType.Deleted:
98+
sb.Append("- ");
99+
break;
100+
}
101+
102+
sb.AppendLine(line.Text);
103+
}
104+
105+
return sb.ToString();
106+
}
107+
108+
private static string[] ListSnapshotInputDirectories()
109+
{
110+
var inputs = Path.Join(RootDirectory(), "snapshots", "input");
111+
return Directory.GetDirectories(inputs);
112+
}
113+
114+
private static void RecursivelyListFiles(string path, List<string> result)
115+
{
116+
if (!Directory.Exists(path)) return;
117+
result.AddRange(Directory.GetFiles(path));
118+
foreach (var directory in Directory.GetDirectories(path))
119+
{
120+
RecursivelyListFiles(directory, result);
121+
}
122+
}
123+
124+
private static string IndexDirectory(string directory)
125+
{
126+
var include = Environment.GetEnvironmentVariable("SCIP_INCLUDE");
127+
var includeOption = include != null ? $" --include {include}" : "";
128+
var arguments = $"run --project ScipDotnet -- index --working-directory {directory}{includeOption}";
129+
var process = new Process()
130+
{
131+
StartInfo = new ProcessStartInfo()
132+
{
133+
FileName = "dotnet",
134+
Arguments = arguments,
135+
UseShellExecute = false,
136+
RedirectStandardOutput = true,
137+
WorkingDirectory = RootDirectory(),
138+
RedirectStandardError = true
139+
}
140+
};
141+
process.Start();
142+
process.WaitForExit();
143+
if (process.ExitCode != 0)
144+
{
145+
var stdout = process.StandardOutput.ReadToEnd();
146+
var stderr = process.StandardError.ReadToEnd();
147+
Assert.Fail(
148+
$"non-zero exit code {process.ExitCode} indexing {directory}\ndotnet {arguments}\n{stdout}{stderr}");
149+
}
150+
151+
return Path.Join(directory, "index.scip");
152+
}
153+
154+
private static string RootDirectory()
155+
{
156+
var process = new Process()
157+
{
158+
StartInfo = new ProcessStartInfo()
159+
{
160+
// The working directory of `dotnet test` is not the root directory of the project
161+
// so we infer it by invoking `git rev-parse --show-toplevel`. It would be cleaner
162+
// to get the root directory from MSBuild but I wasn't able to figure out how to do it
163+
// after searching for ~20 minutes. This works for now and unblocks writing tests.
164+
FileName = "git",
165+
Arguments = "rev-parse --show-toplevel",
166+
UseShellExecute = false,
167+
RedirectStandardOutput = true
168+
}
169+
};
170+
process.Start();
171+
return process.StandardOutput.ReadToEnd().Trim();
172+
}
173+
174+
175+
private Dictionary<String, SymbolInformation> SymbolTable(Document document)
176+
{
177+
var result = new Dictionary<String, SymbolInformation>();
178+
foreach (var info in document.Symbols)
179+
{
180+
// Intentionally crash the test if we emit conflicting SymbolInformation
181+
if (!result.TryAdd(info.Symbol, info))
182+
{
183+
Assert.Fail("duplicate SymbolInformation '{0}'", info.Symbol);
184+
}
185+
}
186+
187+
return result;
188+
}
189+
190+
private string FormatDocument(Index index, Document document)
191+
{
192+
var sb = new StringBuilder();
193+
var inputPath = Path.Join(
194+
index.Metadata.ProjectRoot.Replace("file://", String.Empty),
195+
document.RelativePath);
196+
var occurrences = document.Occurrences.ToList();
197+
occurrences.Sort(CompareOccurrences);
198+
var symtab = SymbolTable(document);
199+
var occurrenceIndex = 0;
200+
var lines = File.ReadAllLines(inputPath);
201+
for (var lineNumber = 0; lineNumber < lines.Length; lineNumber++)
202+
{
203+
var line = lines[lineNumber].Replace("\t", " ");
204+
sb.Append(" ").AppendLine(line);
205+
while (occurrenceIndex < occurrences.Count)
206+
{
207+
var occurrence = occurrences[occurrenceIndex];
208+
var range = Range.FromOccurrence(occurrence);
209+
if (range.Start.Line != range.End.Line)
210+
continue; // TODO: support in the future
211+
if (range.Start.Line != lineNumber)
212+
break;
213+
var isDefinition = (occurrence.SymbolRoles & (int)SymbolRole.Definition) != 0;
214+
var role = isDefinition ? "definition" : "reference";
215+
var length = range.End.Character - range.Start.Character;
216+
var indent = new String(' ', range.Start.Character);
217+
sb.Append("//")
218+
.Append(indent)
219+
.Append(new String('^', length))
220+
.Append(' ')
221+
.Append(role)
222+
.Append(' ')
223+
.AppendLine(occurrence.Symbol);
224+
225+
if (isDefinition)
226+
{
227+
var info = symtab.GetValueOrDefault(occurrence.Symbol, new SymbolInformation());
228+
var prefix = "//" + indent + new String(' ', length + 1);
229+
foreach (var documentation in info.Documentation)
230+
{
231+
sb.Append(prefix).Append("documentation ").AppendLine(documentation.Replace("\n", "\\n"));
232+
}
233+
234+
foreach (var relationship in info.Relationships)
235+
{
236+
sb.Append(prefix).Append("relationship ");
237+
if (relationship.IsDefinition) sb.Append("definition ");
238+
if (relationship.IsImplementation) sb.Append("implementation ");
239+
if (relationship.IsReference) sb.Append("reference ");
240+
if (relationship.IsTypeDefinition) sb.Append("type_definition ");
241+
sb.AppendLine(relationship.Symbol);
242+
}
243+
}
244+
245+
occurrenceIndex++;
246+
}
247+
}
248+
249+
return sb.ToString();
250+
}
251+
252+
private static int CompareOccurrences(Occurrence a, Occurrence b)
253+
{
254+
return Range.FromOccurrence(a).CompareTo(Range.FromOccurrence(b));
255+
}
256+
}

ScipDotnet.Tests/Usings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
global using NUnit.Framework;

0 commit comments

Comments
 (0)