Skip to content

Commit 9087f97

Browse files
authored
Test/csharp extraction (#125)
* Updated beef steak * Made using resolver public in invocation resolver * Added support for nested classes in using filtering * Added constructors to usingresolver symbols * Test for useless imports * Moved test to the right place * Tests on instanceof * More tests on useless imports * More detailed test on extractor * Added nullable types to invocation resolution * Added generic name test and refactored Program.cs test for invocations * Additionnal expectations for extensions * Put my man HeadCrab back in * Added forgotten OuterInnerClass * Changed generic name matching * Fixed tests * More thorough symbol resolution in invocationResolver
1 parent 54a905f commit 9087f97

File tree

11 files changed

+245
-91
lines changed

11 files changed

+245
-91
lines changed

packages/cli/src/languagePlugins/csharp/extensionResolver/index.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ describe("CSharpExtensionResolver", () => {
1010
const extensions = extensionResolver.getExtensions();
1111

1212
test("should resolve extension methods in the project", () => {
13-
expect(Object.keys(extensions).length).toBe(1);
13+
expect(Object.keys(extensions).length).toBe(2);
1414
expect(extensions[""]).toBeDefined();
1515
expect(extensions[""].length).toBe(2);
1616
expect(extensions[""][0].name).toBeDefined();
17+
18+
expect(extensions["MyApp.BeefBurger"]).toBeDefined();
19+
expect(extensions["MyApp.BeefBurger"].length).toBe(1);
20+
expect(extensions["MyApp.BeefBurger"][0].name).toBeDefined();
21+
expect(extensions["MyApp.BeefBurger"][0].name).toBe("Melt");
1722
});
1823
});

packages/cli/src/languagePlugins/csharp/extractor/index.test.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,27 @@ describe("CSharpExtractor", () => {
3131
test("should extract symbols correctly", () => {
3232
expect(
3333
extractor.extractSymbolFromFile(programpath, "Program")?.length,
34-
).toBe(9);
35-
expect(
36-
extractor.extractSymbolFromFile(
37-
twonamespacesonefilepath,
38-
"ChickenBurger.Salad",
39-
)?.length,
40-
).toBe(1);
34+
).toBe(10);
35+
const salad = extractor.extractSymbolFromFile(
36+
twonamespacesonefilepath,
37+
"ChickenBurger.Salad",
38+
);
39+
expect(salad.length).toBe(1);
40+
const saladfile = salad[0];
41+
expect(saladfile.name).toBe("Salad");
42+
expect(saladfile.namespace).toBe("ChickenBurger");
43+
expect(saladfile.subproject.name).toBe("TestFiles");
44+
expect(saladfile.symbol).toMatchObject({
45+
name: "Salad",
46+
type: "class",
47+
namespace: "ChickenBurger",
48+
});
4149
expect(
4250
extractor.extractSymbolFromFile(
4351
twonamespacesonefilepath,
4452
"MyApp.BeefBurger.Steak",
4553
)?.length,
46-
).toBe(1);
54+
).toBe(2);
4755
expect(
4856
extractor.extractSymbolFromFile(seminamespacedpath, "HeadCrab")?.length,
4957
).toBe(2);

packages/cli/src/languagePlugins/csharp/invocationResolver/index.test.ts

Lines changed: 71 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -27,56 +27,25 @@ describe("InvocationResolver", () => {
2727
const usedFiles = invResolver.getInvocationsFromFile(
2828
path.join(csharpFilesFolder, "Program.cs"),
2929
);
30-
expect(usedFiles).toMatchObject({
31-
resolvedSymbols: [
32-
{
33-
name: "Bun",
34-
type: "class",
35-
namespace: "MyApp.BeefBurger",
36-
},
37-
{
38-
name: "Bun",
39-
type: "class",
40-
namespace: "ChickenBurger",
41-
},
42-
{
43-
name: "MyClass",
44-
type: "class",
45-
namespace: "MyNamespace",
46-
},
47-
{
48-
name: "Gordon",
49-
type: "class",
50-
namespace: "HalfNamespace",
51-
},
52-
{
53-
name: "Freeman",
54-
type: "class",
55-
namespace: "",
56-
},
57-
{
58-
name: "OuterInnerClass",
59-
type: "class",
60-
namespace: "OuterNamespace",
61-
},
62-
{
63-
name: "InnerClass",
64-
type: "class",
65-
namespace: "OuterNamespace.InnerNamespace",
66-
},
67-
{
68-
name: "OrderStatus",
69-
type: "enum",
70-
namespace: "MyApp.Models",
71-
},
72-
{
73-
name: "HeadCrab",
74-
type: "class",
75-
namespace: "",
76-
},
77-
],
78-
unresolved: ["System.Math"],
79-
});
30+
const resolved = usedFiles.resolvedSymbols.map((s) =>
31+
s.namespace !== "" ? s.namespace + "." + s.name : s.name,
32+
);
33+
const unresolved = usedFiles.unresolved;
34+
expect(resolved).toContain("MyApp.BeefBurger.Bun");
35+
expect(resolved).toContain("ChickenBurger.Bun");
36+
expect(resolved).toContain("ChickenBurger.Salad");
37+
expect(resolved).toContain("MyNamespace.MyClass");
38+
expect(resolved).toContain("HalfNamespace.Gordon");
39+
expect(resolved).toContain("Freeman");
40+
expect(resolved).toContain("OuterNamespace.InnerNamespace.InnerClass");
41+
expect(resolved).toContain("MyApp.Models.OrderStatus");
42+
expect(resolved).toContain("HeadCrab"); // Used through extension Bite()
43+
expect(resolved).toContain("OuterNamespace.OuterInnerClass");
44+
expect(resolved).not.toContain("MyApp.BeefBurger.Salad");
45+
expect(unresolved).toContain("System.Math");
46+
expect(unresolved).not.toContain("string");
47+
expect(unresolved).not.toContain("System");
48+
expect(unresolved).not.toContain("Salad<string>");
8049
});
8150

8251
test("isUsedInFile", () => {
@@ -145,4 +114,56 @@ describe("InvocationResolver", () => {
145114
unresolved: [],
146115
});
147116
});
117+
118+
test("Finds useless using directives", () => {
119+
const filepath = path.join(csharpFilesFolder, "2Namespaces1File.cs");
120+
const usingDirectives =
121+
invResolver.usingResolver.parseUsingDirectives(filepath);
122+
const invocations = invResolver.getInvocationsFromFile(filepath);
123+
expect(usingDirectives.length).toBe(2);
124+
expect(
125+
usingDirectives.filter((d) => invResolver.isUsingUseful(invocations, d))
126+
.length,
127+
).toBe(1);
128+
129+
const programpath = path.join(csharpFilesFolder, "Program.cs");
130+
const programUsingDirectives =
131+
invResolver.usingResolver.parseUsingDirectives(programpath);
132+
const programInvocations = invResolver.getInvocationsFromFile(programpath);
133+
expect(programUsingDirectives.length).toBe(6);
134+
expect(
135+
programUsingDirectives.filter((d) =>
136+
invResolver.isUsingUseful(programInvocations, d),
137+
).length,
138+
).toBe(6);
139+
140+
const usagepath = path.join(csharpFilesFolder, "Usage.cs");
141+
const usageUsingDirectives =
142+
invResolver.usingResolver.parseUsingDirectives(usagepath);
143+
const usageInvocations = invResolver.getInvocationsFromFile(usagepath);
144+
expect(usageUsingDirectives.length).toBe(6);
145+
expect(
146+
usageUsingDirectives.filter((d) =>
147+
invResolver.isUsingUseful(usageInvocations, d),
148+
).length,
149+
).toBe(4);
150+
151+
const globalusingpath = path.join(
152+
csharpFilesFolder,
153+
"Subfolder/GlobalUsings.cs",
154+
);
155+
const globalusingDirectives =
156+
invResolver.usingResolver.parseUsingDirectives(globalusingpath);
157+
const globalusingInvocations =
158+
invResolver.getInvocationsFromFile(globalusingpath);
159+
expect(globalusingDirectives.length).toBe(2);
160+
expect(
161+
globalusingDirectives.filter((d) =>
162+
invResolver.isUsingUseful(globalusingInvocations, d),
163+
).length,
164+
).toBe(1); // Every external import is considered useful no matter what
165+
// Even if there is no invocation.
166+
// It's also not dangerous to remove a global using directive
167+
// because they are regrouped in GlobalUsings.cs.
168+
});
148169
});

packages/cli/src/languagePlugins/csharp/invocationResolver/index.ts

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -41,50 +41,63 @@ const variablesQuery = new Parser.Query(
4141
const calledClassesQuery = new Parser.Query(
4242
csharpParser.getLanguage(),
4343
`
44+
(object_creation_expression
45+
type: (identifier) @cls)
46+
(object_creation_expression
47+
type: (qualified_name) @cls)
4448
((object_creation_expression
45-
type: (identifier) @cls))
46-
((object_creation_expression
47-
type: (qualified_name) @cls))
49+
type: (generic_name) @cls))
4850
(variable_declaration
4951
type: (identifier) @cls)
5052
(variable_declaration
5153
type: (qualified_name) @cls)
54+
(variable_declaration
55+
type: (generic_name) @cls)
5256
(parameter
5357
type: (identifier) @cls)
5458
(parameter
5559
type: (qualified_name) @cls)
60+
(parameter
61+
type: (generic_name) @cls)
5662
(type_argument_list
5763
(identifier) @cls)
5864
(type_argument_list
5965
(qualified_name) @cls)
66+
(type_argument_list
67+
(generic_name) @cls)
6068
(base_list
6169
(identifier) @cls)
6270
(base_list
6371
(qualified_name) @cls)
72+
(base_list
73+
(generic_name) @cls)
6474
(property_declaration
6575
type: (qualified_name) @cls)
6676
(property_declaration
6777
type: (identifier) @cls)
6878
(property_declaration
69-
type: (nullable_type
70-
type: (identifier) @cls))
71-
(property_declaration
72-
type: (nullable_type
73-
type: (qualified_name) @cls))
79+
type: (generic_name) @cls)
7480
(typeof_expression
7581
type: (_) @cls)
7682
(method_declaration
7783
returns: (qualified_name) @cls)
7884
(method_declaration
7985
returns: (identifier) @cls)
86+
(method_declaration
87+
returns: (generic_name) @cls)
8088
(array_type
8189
type: (identifier) @cls)
8290
(array_type
8391
type: (qualified_name) @cls)
84-
(generic_name) @cls
92+
(array_type
93+
type: (generic_name) @cls)
94+
(nullable_type
95+
type: (identifier) @cls)
96+
(nullable_type
97+
type: (qualified_name) @cls)
98+
(nullable_type
99+
type: (generic_name) @cls)
85100
`,
86-
// Might have to change the "(generic_name) @cls" line
87-
// to be smarter, it may or may not add 1s of runtime.
88101
);
89102

90103
/**
@@ -138,7 +151,7 @@ export interface Invocations {
138151
export class CSharpInvocationResolver {
139152
parser: Parser = csharpParser;
140153
public nsMapper: CSharpNamespaceMapper;
141-
private usingResolver: CSharpUsingResolver;
154+
public usingResolver: CSharpUsingResolver;
142155
private extensions: ExtensionMethodMap = {};
143156
private resolvedImports: ResolvedImports;
144157
private cache: Map<string, Invocations> = new Map<string, Invocations>();
@@ -179,17 +192,33 @@ export class CSharpInvocationResolver {
179192
// Remove any generic type information from the classname
180193
// Classes in the type argument list are managed on their own.
181194
const cleanClassname = classname.split("<")[0];
195+
let cutClassname = cleanClassname;
182196
// Try to find the class in the resolved imports
183-
const ucls = this.usingResolver.findClassInImports(
184-
this.resolvedImports,
185-
cleanClassname,
186-
filepath,
187-
);
197+
let ucls = null;
198+
while (!ucls) {
199+
ucls = this.usingResolver.findClassInImports(
200+
this.resolvedImports,
201+
cleanClassname,
202+
filepath,
203+
);
204+
if (!cutClassname.includes(".")) {
205+
break;
206+
}
207+
cutClassname = cutClassname.split(".").slice(0, -1).join(".");
208+
}
188209
if (ucls) {
189210
return ucls;
190211
}
191212
// Try to find the class in the namespace tree
192-
const cls = this.nsMapper.findClassInTree(namespaceTree, cleanClassname);
213+
let cls = null;
214+
cutClassname = cleanClassname;
215+
while (!cls) {
216+
cls = this.nsMapper.findClassInTree(namespaceTree, cutClassname);
217+
if (!cutClassname.includes(".")) {
218+
break;
219+
}
220+
cutClassname = cutClassname.split(".").slice(0, -1).join(".");
221+
}
193222
if (cls) {
194223
return cls;
195224
}
@@ -488,7 +517,10 @@ export class CSharpInvocationResolver {
488517
(usedNamespace.namespace &&
489518
inv.namespace ===
490519
this.nsMapper.getFullNSName(usedNamespace.namespace)) ||
491-
(usedNamespace.symbol && inv.name === usedNamespace.symbol.name),
520+
(usedNamespace.symbol && inv.name === usedNamespace.symbol.name) ||
521+
(usedNamespace.symbol &&
522+
inv.parent &&
523+
inv.parent.name === usedNamespace.symbol.name),
492524
);
493525
}
494526
}

packages/cli/src/languagePlugins/csharp/metricsAnalyzer/index.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ describe("CSharpMetricsAnalyzer", () => {
2020
const metrics = analyzeFile("2Namespaces1File.cs");
2121
expect(metrics).toMatchObject({
2222
cyclomaticComplexity: 0, // No control flow statements in this file
23-
linesCount: 13, // Total lines in the file
24-
codeLinesCount: 12, // Excluding blank lines and comments
23+
linesCount: 42, // Total lines in the file
24+
codeLinesCount: 38, // Excluding blank lines and comments
2525
characterCount: expect.any(Number), // Total characters in the file
2626
codeCharacterCount: expect.any(Number), // Characters excluding comments and whitespace
2727
});
@@ -75,8 +75,8 @@ describe("CSharpMetricsAnalyzer", () => {
7575
const metrics = analyzeFile("Program.cs");
7676
expect(metrics).toMatchObject({
7777
cyclomaticComplexity: 0, // No control flow statements in the main method
78-
linesCount: 34,
79-
codeLinesCount: 26,
78+
linesCount: 35,
79+
codeLinesCount: 27,
8080
characterCount: expect.any(Number),
8181
codeCharacterCount: expect.any(Number),
8282
});

packages/cli/src/languagePlugins/csharp/namespaceMapper/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export interface SymbolNode {
3333
filepath: string;
3434
/** The syntax node corresponding to the symbol */
3535
node: Parser.SyntaxNode;
36+
/** The parent of the symbol if it is nested */
37+
parent?: SymbolNode;
3638
}
3739

3840
const DEBUG_NAMESPACE = "namespace";
Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,41 @@
1+
using HalfNamespace;
2+
using MyApp.Models; // Useless using directive
3+
14
namespace MyApp.BeefBurger
25
{
3-
public class Steak { }
4-
public class Cheese { }
6+
public class Steak
7+
{
8+
private Gordon gordon;
9+
10+
public Steak(Gordon gordon)
11+
{
12+
this.gordon = gordon;
13+
}
14+
15+
public void Cook()
16+
{
17+
Console.WriteLine("Gordon, we need to cook.");
18+
}
19+
}
20+
public static class Cheese
21+
{
22+
public static void Melt(this Steak steak)
23+
{
24+
Console.WriteLine("Cheese melted.");
25+
}
26+
}
527
public class Bun { }
628
}
729
namespace ChickenBurger
830
{
931
public class Chicken { }
10-
public class Salad { }
32+
public class Salad<T>
33+
{
34+
public T Item { get; set; }
35+
public void Add(T item)
36+
{
37+
Item = item;
38+
}
39+
}
1140
public class Bun { }
1241
}

0 commit comments

Comments
 (0)