Skip to content

Commit 83207e6

Browse files
authored
Handle attributes (#1)
Handling for attributes Note: method parameter attributes are not supported
1 parent ca0f32b commit 83207e6

38 files changed

+211
-82
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -368,3 +368,4 @@ FodyWeavers.xsd
368368
/Documentation/.$CSharpAnalyst.drawio.bkp
369369
*.dtmp
370370
/tmp
371+
/SampleProject/project.json

CSharpCodeAnalyst/GraphArea/MsaglBuilder.cs

+5
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,11 @@ private static string GetLabelText(Dependency dependency)
274274
return string.Empty;
275275
}
276276

277+
if (dependency.Type == DependencyType.UsesAttribute)
278+
{
279+
return string.Empty;
280+
}
281+
277282
return dependency.Type.ToString();
278283
}
279284

CSharpCodeAnalyst/MainViewModel.cs

+3-5
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ internal class MainViewModel : INotifyPropertyChanged
3535
private readonly MessageBus _messaging;
3636

3737
private readonly ProjectExclusionRegExCollection _projectExclusionFilters;
38-
private readonly int _warningCodeElementLimit;
3938
private CodeGraph? _codeGraph;
4039
private CycleSummaryViewModel? _cycleSummaryViewModel;
4140

@@ -67,7 +66,6 @@ internal MainViewModel(MessageBus messaging, ApplicationSettings? settings)
6766
{
6867
_isInfoPanelVisible = settings.DefaultShowQuickHelp;
6968
_projectExclusionFilters.Initialize(settings.DefaultProjectExcludeFilter, ";");
70-
_warningCodeElementLimit = settings.WarningCodeElementLimit;
7169
}
7270

7371
_messaging = messaging;
@@ -614,7 +612,7 @@ private void LoadProject()
614612
throw new NullReferenceException();
615613
}
616614

617-
var codeGraph = projectData.CreateCodeStructure();
615+
var codeGraph = projectData.CreateCodeGraph();
618616

619617

620618
// Load settings
@@ -666,7 +664,7 @@ private void SaveProject()
666664
}
667665

668666
var projectData = new ProjectData();
669-
projectData.AddCodeStructure(_codeGraph);
667+
projectData.AddCodeGraph(_codeGraph);
670668
projectData.Settings[nameof(IsInfoPanelVisible)] = IsInfoPanelVisible.ToString();
671669
projectData.Settings[nameof(GraphViewModel.ShowFlatGraph)] = _graphViewModel.ShowFlatGraph.ToString();
672670
projectData.Settings[nameof(ProjectExclusionRegExCollection)] = _projectExclusionFilters.ToString();
@@ -713,7 +711,7 @@ internal bool OnClosing()
713711
{
714712
if (_isSaved is false)
715713
{
716-
if (MessageBox.Show("Do you wan't to save the project so you don't have to import it again?", "Save",
714+
if (MessageBox.Show("Do you want to save the project so you don't have to import it again?", "Save",
717715
MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
718716
{
719717
SaveProject();

CSharpCodeAnalyst/Project/ProjectData.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ public class ProjectData
1515
/// <summary>
1616
/// Flatten the recursive structures.
1717
/// </summary>
18-
public void AddCodeStructure(CodeGraph codeGraph)
18+
public void AddCodeGraph(CodeGraph codeGraph)
1919
{
2020
CodeElements = codeGraph.Nodes.Values
21-
.Select(n => new SerializableCodeElement(n.Id, n.Name, n.FullName, n.ElementType, n.SourceLocations)
21+
.Select(n => new SerializableCodeElement(n.Id, n.Name, n.FullName, n.ElementType, n.SourceLocations, n.Attributes)
2222
{ SourceLocations = n.SourceLocations }).ToList();
2323

2424
// We iterate over children, so we expect to have a parent
@@ -33,7 +33,7 @@ public void AddCodeStructure(CodeGraph codeGraph)
3333
.ToList();
3434
}
3535

36-
public CodeGraph CreateCodeStructure()
36+
public CodeGraph CreateCodeGraph()
3737
{
3838
var codeStructure = new CodeGraph();
3939

@@ -42,6 +42,7 @@ public CodeGraph CreateCodeStructure()
4242
{
4343
var element = new CodeElement(se.Id, se.ElementType, se.Name, se.FullName, null!);
4444
element.SourceLocations = se.SourceLocations;
45+
element.Attributes = se.Attributes;
4546
codeStructure.Nodes.Add(element.Id, element);
4647
}
4748

CSharpCodeAnalyst/Project/SerializableCodeElement.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ public class SerializableCodeElement(
88
string name,
99
string fullName,
1010
CodeElementType elementType,
11-
List<SourceLocation> sourceLocations)
11+
List<SourceLocation> sourceLocations,
12+
HashSet<string> attributes)
1213
{
1314
public string Id { get; set; } = id;
1415
public string Name { get; set; } = name;
1516
public string FullName { get; set; } = fullName;
1617
public CodeElementType ElementType { get; set; } = elementType;
1718
public List<SourceLocation> SourceLocations { get; set; } = sourceLocations;
19+
public HashSet<string> Attributes { get; set; } = attributes;
1820
}

CSharpCodeAnalyst/board.txt

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
IMPROVEMENTS
22
---------------------
33

4-
- Recognize Attributes
5-
6-
- Do not collapse single node in cycle graph
74
- Highlight of aggregated edges takes too long. If quick help not visible skip?
85
- Performance in general
96

107

8+
- Attributes are caught at class or method level. Not for the parameters like [CallerMemberName]
9+
10+
1111
BUGS
1212
---------------------
1313

CodeParser/Export/DgmlExport.cs

+28-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public void Export(string fileName, CodeGraph graph)
4040
foreach (var edge in normal)
4141
{
4242
// Omit the calls label for better readability.
43-
var edgeLabel = edge.Type == DependencyType.Calls ? string.Empty : edge.Type.ToString();
43+
var edgeLabel = GetEdgeLabel(edge);
4444
builder.AddEdgeById(edge.SourceId, edge.TargetId, edgeLabel);
4545
}
4646

@@ -60,6 +60,33 @@ public void Export(string fileName, CodeGraph graph)
6060
builder.WriteOutput(fileName);
6161
}
6262

63+
private static string GetEdgeLabel(Dependency dependency)
64+
{
65+
// Omit the label text for now. The color makes it clear that it is a call dependency
66+
if (dependency.Type == DependencyType.Calls)
67+
{
68+
return string.Empty;
69+
}
70+
71+
// We can see this by the dotted line
72+
if (dependency.Type == DependencyType.Implements || dependency.Type == DependencyType.Inherits)
73+
{
74+
return string.Empty;
75+
}
76+
77+
if (dependency.Type == DependencyType.Uses)
78+
{
79+
return string.Empty;
80+
}
81+
82+
if (dependency.Type == DependencyType.UsesAttribute)
83+
{
84+
return string.Empty;
85+
}
86+
87+
return dependency.Type.ToString();
88+
}
89+
6390
private static void WriteCategories(DgmlFileBuilder writer)
6491
{
6592
var elementTypes = Enum.GetValues(typeof(CodeElementType)).Cast<CodeElementType>();

CodeParser/Parser/Parser.Phase1.cs

+3-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace CodeParser.Parser;
77

88
public partial class Parser
99
{
10-
private readonly List<INamedTypeSymbol> AllNamedTypesInSolution = new();
10+
private readonly List<INamedTypeSymbol> _allNamedTypesInSolution = new();
1111

1212
private async Task BuildHierarchy(Solution solution)
1313
{
@@ -28,7 +28,7 @@ private async Task BuildHierarchy(Solution solution)
2828
// Build also a list of all named types in the solution
2929
// We need this in phase 2 to resolve dependencies
3030
var types = compilation.GetSymbolsWithName(_ => true, SymbolFilter.Type).OfType<INamedTypeSymbol>();
31-
AllNamedTypesInSolution.AddRange(types);
31+
_allNamedTypesInSolution.AddRange(types);
3232

3333

3434
BuildHierarchy(compilation);
@@ -145,8 +145,6 @@ private void ProcessNodeForHierarchy(SyntaxNode node, SemanticModel semanticMode
145145
case EventDeclarationSyntax:
146146
symbol = semanticModel.GetDeclaredSymbol(node) as IEventSymbol;
147147
elementType = CodeElementType.Event;
148-
149-
150148
break;
151149
// Add more cases as needed (e.g., for events, delegates, etc.)
152150
}
@@ -161,6 +159,7 @@ private void ProcessNodeForHierarchy(SyntaxNode node, SemanticModel semanticMode
161159
}
162160
else
163161
{
162+
// The parent gets the indirect children assigned as children
164163
foreach (var childNode in node.ChildNodes())
165164
{
166165
ProcessNodeForHierarchy(childNode, semanticModel, parent);

CodeParser/Parser/Parser.Phase2.cs

+23-4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ private void AnalyzeDependencies(Solution solution)
4444
AnalyzeFieldDependencies(element, fieldSymbol);
4545
}
4646

47+
// For all type of symbols check if decorated with an attribute.
48+
AnalyzeAttributeDependencies(element, symbol);
49+
4750
if (loop % 10 == 0)
4851
{
4952
ParserProgress?.Invoke(this, new ParserProgressArg
@@ -56,6 +59,22 @@ private void AnalyzeDependencies(Solution solution)
5659
}
5760
}
5861

62+
private void AnalyzeAttributeDependencies(CodeElement element, ISymbol symbol)
63+
{
64+
foreach (var attributeData in symbol.GetAttributes())
65+
{
66+
if (attributeData.AttributeClass != null)
67+
{
68+
var location = attributeData.ApplicationSyntaxReference != null
69+
? GetLocation(attributeData.ApplicationSyntaxReference.GetSyntax())
70+
: null;
71+
72+
element.Attributes.Add(attributeData.AttributeClass.Name);
73+
AddTypeDependency(element, attributeData.AttributeClass, DependencyType.UsesAttribute, location);
74+
}
75+
}
76+
}
77+
5978
private void AnalyzeDelegateDependencies(CodeElement delegateElement, INamedTypeSymbol delegateSymbol)
6079
{
6180
var methodSymbol = delegateSymbol.DelegateInvokeMethod;
@@ -119,7 +138,7 @@ private void AnalyzeMethodDependencies(Solution solution, CodeElement methodElem
119138
// If this method is an interface method or an abstract method, find its implementations
120139
if (methodSymbol.IsAbstract || methodSymbol.ContainingType.TypeKind == TypeKind.Interface)
121140
{
122-
FindImplementations(solution, methodElement, methodSymbol);
141+
FindImplementations(methodElement, methodSymbol);
123142
}
124143

125144
// Check for method override
@@ -150,7 +169,7 @@ private void AnalyzeMethodDependencies(Solution solution, CodeElement methodElem
150169
}
151170

152171

153-
private void FindImplementations(Solution solution, CodeElement methodElement, IMethodSymbol methodSymbol)
172+
private void FindImplementations(CodeElement methodElement, IMethodSymbol methodSymbol)
154173
{
155174
var implementingTypes = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
156175

@@ -183,13 +202,13 @@ private void FindImplementations(Solution solution, CodeElement methodElement, I
183202

184203
private IEnumerable<INamedTypeSymbol> FindTypesImplementingInterface(INamedTypeSymbol interfaceSymbol)
185204
{
186-
return AllNamedTypesInSolution
205+
return _allNamedTypesInSolution
187206
.Where(type => type.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, interfaceSymbol)));
188207
}
189208

190209
private IEnumerable<INamedTypeSymbol> FindTypesDerivedFrom(INamedTypeSymbol baseType)
191210
{
192-
return AllNamedTypesInSolution
211+
return _allNamedTypesInSolution
193212
.Where(type => IsTypeDerivedFrom(type, baseType));
194213
}
195214

CodeParserTests/CodeParserApprovalTests.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ public void FindsAllFunctions()
7777
"CSharpLanguage.CSharpLanguage.MissingInterface.IStorage.Load",
7878
"CSharpLanguage.CSharpLanguage.NS_Parent.NS_Child.ClassNsChild.Method",
7979

80+
"CSharpLanguage.CSharpLanguage.IStructInterface.Method",
81+
"CSharpLanguage.CSharpLanguage.StructWithInterface.Method",
82+
8083
// Extension method
8184
"CSharpLanguage.CSharpLanguage.Extensions.Slice",
8285

@@ -177,7 +180,8 @@ public void FindsAllMethodImplementations()
177180
var expected = new HashSet<string>
178181
{
179182
"ModuleLevel1.ModuleLevel1.ServiceBase.Do -> ModuleLevel1.ModuleLevel1.IServiceC.Do",
180-
"CSharpLanguage.CSharpLanguage.MissingInterface.BaseStorage.Load -> CSharpLanguage.CSharpLanguage.MissingInterface.IStorage.Load"
183+
"CSharpLanguage.CSharpLanguage.MissingInterface.BaseStorage.Load -> CSharpLanguage.CSharpLanguage.MissingInterface.IStorage.Load",
184+
"CSharpLanguage.CSharpLanguage.StructWithInterface.Method -> CSharpLanguage.CSharpLanguage.IStructInterface.Method"
181185
};
182186

183187

Contracts/Graph/CodeElement.cs

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ public class CodeElement(string id, CodeElementType elementType, string name, st
77
{
88
public List<SourceLocation> SourceLocations { get; set; } = [];
99

10+
/// <summary>
11+
/// Unlike in the dependency graph where external dependencies are omitted
12+
/// I want to keep all attributes here.
13+
/// </summary>
14+
public HashSet<string> Attributes { get; set; } = [];
15+
1016
public HashSet<CodeElement> Children { get; } = [];
1117

1218
public HashSet<Dependency> Dependencies { get; } = [];

Contracts/Graph/DependencyType.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,7 @@ public enum DependencyType
1515
// Special dependency to model the hierarchy.
1616
// In the CodeElement this dependency is modeled via.
1717
// Parent / Children
18-
Containment
18+
Containment,
19+
20+
UsesAttribute,
1921
}

Documentation/Cycle detection.md

+9-9
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ The current cycle detection algorithm for code graphs has room for improvement.
88

99
Initially, we attempted to detect cycles between code elements based on dependencies analyzed by the code parser. The naive assumption was that cycles between dependencies would induce cycles in higher-level containers (e.g., classes, namespaces).
1010

11-
![](Examples\class-by-field.png)
11+
![](Images/class-by-field.png)
1212

1313
The two classes reference each other by their fields and form a class cycle. If the classes are in different namespaces there would be a cycle between the namespaces. The dependency arrows would cross the namespace boundaries.
1414

1515
The algorithm could do this automatically if we include the parent-child relationships in the search graph.
1616

17-
![](Examples\class-by-field-with-containment.png)
17+
![](Images/class-by-field-with-containment.png)
1818

1919
However other situations do not automatically connect all related elements. In the image below there is no cycle formed, even by including the parent-child relationships.
2020

21-
![](Examples\class-by-method-with-containment.png)
21+
![](Images/class-by-method-with-containment.png)
2222

2323
Adding artificial **is-child-of** dependencies would cause cycles everywhere.
2424

@@ -32,7 +32,7 @@ Assume the following code graph is extracted from the source code.
3232

3333
There is a cycle between **Namespace 2** and **Namespace 3,** caused by **Method 1** calling **Method 2** and **_field1** holding a reference to **Class 1.**
3434

35-
![](Examples\solution-problem.png)
35+
![](Images/solution-problem.png)
3636

3737

3838

@@ -48,7 +48,7 @@ Process:
4848

4949
Here is what the search graph looks like:
5050

51-
![](Examples\search-graph.png)
51+
![](Images/search-graph.png)
5252

5353
Assume the call from **Method 1** to **Method 2.** The least common ancestor is **Namespace 1**, which is excluded. This results in **Namespace 2** and **Namespace 3** being the highest involved code elements in this **call** dependency.
5454

@@ -80,7 +80,7 @@ Assume the following code graph. Here we have a namespace cycle between **Namesp
8080

8181

8282

83-
![](Examples\edge-case-source-graph.png)
83+
![](Images/edge-case-source-graph.png)
8484

8585
#### Handling containment in step 1
8686

@@ -118,7 +118,7 @@ Following this procedure, only the green elements in the image below are conside
118118

119119
The set difference operation is necessary because the sources should not be included in the targets, as **Namespace 2** is a child of **Namespace 1**.
120120

121-
![](Examples\containment.png)
121+
![](Images/containment.png)
122122

123123

124124

@@ -128,7 +128,7 @@ In the previously discussed scenario, namespaces play a crucial role. Is it poss
128128

129129
Assume following scenario:
130130

131-
![](Examples\nested_classes_code_graph.png)
131+
![](Images/nested_classes_code_graph.png)
132132

133133

134134

@@ -138,7 +138,7 @@ The proxy dependencies are between **DirectClass** and **MiddleClass**.
138138

139139
The resulting cycle looks like this
140140

141-
![](Examples\nested_classes_scc.png)
141+
![](Images/nested_classes_scc.png)
142142

143143
## Conclusion
144144

0 commit comments

Comments
 (0)