Skip to content

Commit 5e54364

Browse files
author
LoneWandererProductions
committed
Add some new rules.
Improve my viewer
1 parent dcc778d commit 5e54364

10 files changed

Lines changed: 393 additions & 64 deletions

Core.Apps/CommandFactory.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ public static IReadOnlyList<ICommand> GetCommands(Weave? weave = null)
4848
new HotPathAnalyzer(), new LicenseHeaderAnalyzer(), new UnusedClassAnalyzer(),
4949
new UnusedConstantAnalyzer(), new UnusedLocalVariableAnalyzer(), new UnusedParameterAnalyzer(),
5050
new UnusedPrivateFieldAnalyzer(), new DocCommentCoverageCommand(), new DeadReferenceAnalyzer(),
51-
new ApiExplorerCommand(), new LogTailCommand(), new SmartPingPro(), new Tree()
51+
new ApiExplorerCommand(), new LogTailCommand(), new SmartPingPro(), new Tree(), new StructPaddingAnalyzer(),
52+
new UnusedMemberAnalyzer(), new MagicNumberAnalyzer()
5253
});
5354

5455
// --- PRODUCERS (Require Registry) ---
@@ -106,7 +107,8 @@ public static IReadOnlyList<ICodeAnalyzer> GetAllAnalyzers()
106107
new DuplicateStringLiteralAnalyzer(), new EventHandlerAnalyzer(), new HotPathAnalyzer(),
107108
new LicenseHeaderAnalyzer(), new UnusedClassAnalyzer(), new UnusedConstantAnalyzer(),
108109
new UnusedLocalVariableAnalyzer(), new UnusedParameterAnalyzer(), new UnusedPrivateFieldAnalyzer(),
109-
new DocCommentCoverageCommand(), new DeadReferenceAnalyzer()
110+
new DocCommentCoverageCommand(), new DeadReferenceAnalyzer(), new StructPaddingAnalyzer(),
111+
new UnusedMemberAnalyzer(), new MagicNumberAnalyzer()
110112
};
111113

112114
return modules;

Core.Apps/Helper/CoreHelper.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,22 @@ internal static IEnumerable<string> GetSourceFiles(string projectPath)
225225
return files;
226226
}
227227

228+
/// <summary>
229+
/// Determines whether [is symbol used] [the specified model].
230+
/// </summary>
231+
/// <param name="model">The model.</param>
232+
/// <param name="root">The root.</param>
233+
/// <param name="symbol">The symbol.</param>
234+
/// <returns>
235+
/// <c>true</c> if [is symbol used] [the specified model]; otherwise, <c>false</c>.
236+
/// </returns>
237+
internal static bool IsSymbolUsed(SemanticModel model, SyntaxNode root, ISymbol symbol)
238+
{
239+
return root.DescendantNodes()
240+
.OfType<IdentifierNameSyntax>()
241+
.Any(id => SymbolEqualityComparer.Default.Equals(model.GetSymbolInfo(id).Symbol, symbol));
242+
}
243+
228244
/// <summary>
229245
/// Determines whether a <c>for</c> loop has a constant numeric bound.
230246
/// </summary>

Core.Apps/Rules/LicenseHeaderAnalyzer.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ public IEnumerable<Diagnostic> Analyze(string filePath, string fileContent)
6969
/// <summary>
7070
/// Executes the analyzer on a directory path using centralized RunAnalyze logic.
7171
/// </summary>
72-
/// <inheritdoc />
7372
public CommandResult Execute(params string[] args)
7473
{
7574
List<Diagnostic> results;
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* COPYRIGHT: See COPYING in the top level directory
3+
* PROJECT: Core.Apps.Rules
4+
* FILE: MagicNumberAnalyzer.cs
5+
* PURPOSE: Detects unexplained numeric literals (magic numbers).
6+
* PROGRAMMER: Peter Geinitz (Wayfarer)
7+
*/
8+
9+
using System;
10+
using System.Collections.Generic;
11+
using System.IO;
12+
using System.Linq;
13+
using Core.Apps.Enums;
14+
using Core.Apps.Helper;
15+
using Core.Apps.Interface;
16+
using Microsoft.CodeAnalysis;
17+
using Microsoft.CodeAnalysis.CSharp;
18+
using Microsoft.CodeAnalysis.CSharp.Syntax;
19+
using Weaver;
20+
using Weaver.Interfaces;
21+
using Weaver.Messages;
22+
23+
namespace Core.Apps.Rules
24+
{
25+
/// <inheritdoc cref="ICodeAnalyzer" />
26+
/// <summary>
27+
/// Analyzer that detects unexplained numeric literals (magic numbers) in method bodies, which can hurt readability and maintainability.
28+
/// It ignores common "safe" numbers like 0, 1, -1, and 2, as well as literals that are part of constant definitions.
29+
/// </summary>
30+
/// <seealso cref="ICommand" />
31+
public sealed class MagicNumberAnalyzer : ICommand, ICodeAnalyzer
32+
{
33+
/// <inheritdoc cref="ICodeAnalyzer" />
34+
public string Namespace => "Analyzer";
35+
36+
/// <inheritdoc cref="ICodeAnalyzer" />
37+
public string Name => "MagicNumber";
38+
39+
/// <inheritdoc cref="ICodeAnalyzer" />
40+
public string Description => "Detects unexplained numeric literals in method bodies.";
41+
42+
/// <inheritdoc />
43+
public int ParameterCount => 1;
44+
45+
/// <inheritdoc />
46+
public CommandSignature Signature => new(Namespace, Name, ParameterCount);
47+
48+
/// <summary>
49+
/// The safe numbers
50+
/// Constants that are usually considered "safe" or self-documenting
51+
/// </summary>
52+
private static readonly HashSet<string> SafeNumbers = new() { "0", "1", "-1", "2" };
53+
54+
/// <inheritdoc />
55+
public IEnumerable<Diagnostic> Analyze(string filePath, string fileContent)
56+
{
57+
if (CoreHelper.ShouldIgnoreFile(filePath)) yield break;
58+
59+
var tree = CSharpSyntaxTree.ParseText(fileContent);
60+
var compilation = CSharpCompilation.Create("Analysis")
61+
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
62+
.AddSyntaxTrees(tree);
63+
64+
var model = compilation.GetSemanticModel(tree);
65+
var root = tree.GetCompilationUnitRoot();
66+
67+
// Find all methods
68+
var methods = root.DescendantNodes().OfType<MethodDeclarationSyntax>();
69+
70+
foreach (var method in methods)
71+
{
72+
// Find all numeric literals inside the method body
73+
var literals = method.DescendantNodes().OfType<LiteralExpressionSyntax>()
74+
.Where(l => l.IsKind(SyntaxKind.NumericLiteralExpression));
75+
76+
foreach (var literal in literals)
77+
{
78+
string value = literal.Token.ValueText;
79+
80+
// Skip "safe" numbers and ignore if it's already part of a constant definition
81+
if (SafeNumbers.Contains(value) || IsInConstantDefinition(literal))
82+
continue;
83+
84+
yield return new Diagnostic(
85+
Name,
86+
Enums.DiagnosticSeverity.Info,
87+
filePath,
88+
literal.GetLocation().GetLineSpan().StartLinePosition.Line + 1,
89+
$"Magic number '{value}' detected. Replace with a named constant.",
90+
DiagnosticImpact.Readability
91+
);
92+
}
93+
}
94+
}
95+
96+
/// <summary>
97+
/// Determines whether [is in constant definition] [the specified node].
98+
/// </summary>
99+
/// <param name="node">The node.</param>
100+
/// <returns>
101+
/// <c>true</c> if [is in constant definition] [the specified node]; otherwise, <c>false</c>.
102+
/// </returns>
103+
private static bool IsInConstantDefinition(SyntaxNode node)
104+
{
105+
// Don't flag if the literal is actually being assigned to a 'const' variable
106+
return node.Ancestors().OfType<FieldDeclarationSyntax>().Any(f => f.Modifiers.Any(SyntaxKind.ConstKeyword));
107+
}
108+
109+
/// <inheritdoc />
110+
public CommandResult Execute(params string[] args)
111+
{
112+
try
113+
{
114+
var results = AnalyzerExecutor.ExecutePath(this, args, "Usage: MagicNumber <fileOrDirectoryPath>");
115+
var output = results.Count > 0
116+
? string.Join("\n", results.Select(d => $"{Path.GetFileName(d.FilePath)} ({d.LineNumber}): {d.Message}"))
117+
: "No magic numbers found.";
118+
return CommandResult.Ok(output, EnumTypes.Wstring);
119+
}
120+
catch (Exception ex)
121+
{
122+
return CommandResult.Fail(ex.Message);
123+
}
124+
}
125+
}
126+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* COPYRIGHT: See COPYING in the top level directory
3+
* PROJECT: Core.Apps.Rules
4+
* FILE: StructPaddingAnalyzer.cs
5+
* PURPOSE: Analyzes struct field ordering to minimize memory padding.
6+
* PROGRAMMER: Peter Geinitz (Wayfarer)
7+
*/
8+
9+
using System;
10+
using System.Collections.Generic;
11+
using System.Linq;
12+
using Core.Apps.Enums;
13+
using Core.Apps.Helper;
14+
using Core.Apps.Interface;
15+
using Microsoft.CodeAnalysis;
16+
using Microsoft.CodeAnalysis.CSharp;
17+
using Microsoft.CodeAnalysis.CSharp.Syntax;
18+
using Weaver;
19+
using Weaver.Interfaces;
20+
using Weaver.Messages;
21+
22+
namespace Core.Apps.Rules
23+
{
24+
/// <inheritdoc cref="ICodeAnalyzer" />
25+
/// <summary>
26+
/// Analyzer that detects inefficient struct field ordering that causes unnecessary memory padding.
27+
/// </summary>
28+
/// <seealso cref="ICommand" />
29+
public sealed class StructPaddingAnalyzer : ICodeAnalyzer, ICommand
30+
{
31+
32+
/// <inheritdoc cref="ICodeAnalyzer" />
33+
public string Name => "StructPadding";
34+
35+
/// <inheritdoc cref="ICodeAnalyzer" />
36+
public string Description => "Analyzes struct field ordering to optimize memory alignment.";
37+
38+
/// <inheritdoc cref="ICodeAnalyzer" />
39+
public string Namespace => "Analyzer";
40+
41+
/// <inheritdoc />
42+
public int ParameterCount => 1;
43+
44+
/// <inheritdoc />
45+
public CommandSignature Signature => new(Namespace, Name, ParameterCount);
46+
47+
/// <inheritdoc />
48+
public IEnumerable<Diagnostic> Analyze(string filePath, string fileContent)
49+
{
50+
if (CoreHelper.ShouldIgnoreFile(filePath)) yield break;
51+
52+
var tree = CSharpSyntaxTree.ParseText(fileContent);
53+
var root = tree.GetCompilationUnitRoot();
54+
55+
foreach (var structDecl in root.DescendantNodes().OfType<StructDeclarationSyntax>())
56+
{
57+
var fields = structDecl.Members.OfType<FieldDeclarationSyntax>();
58+
var fieldList = fields.SelectMany(f => f.Declaration.Variables.Select(v => new {
59+
Name = v.Identifier.Text,
60+
Size = GetFieldSize(f.Declaration.Type.ToString())
61+
})).ToList();
62+
63+
if (fieldList.Count <= 1) continue;
64+
65+
var optimalOrder = fieldList.OrderByDescending(f => f.Size).ToList();
66+
67+
// Check if current matches optimal
68+
bool isOptimal = true;
69+
for (int i = 0; i < fieldList.Count; i++)
70+
{
71+
if (fieldList[i].Name != optimalOrder[i].Name)
72+
{
73+
isOptimal = false;
74+
break;
75+
}
76+
}
77+
78+
if (!isOptimal)
79+
{
80+
var line = structDecl.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
81+
string suggestion = string.Join(", ", optimalOrder.Select(f => f.Name));
82+
83+
yield return new Diagnostic(
84+
Name,
85+
Enums.DiagnosticSeverity.Info,
86+
filePath,
87+
line,
88+
$"Struct '{structDecl.Identifier.Text}' is not optimized. " +
89+
$"Reorder fields to: {suggestion}",
90+
DiagnosticImpact.Other
91+
);
92+
}
93+
}
94+
}
95+
96+
/// <summary>
97+
/// Gets the size of the field.
98+
/// </summary>
99+
/// <param name="typeName">Name of the type.</param>
100+
/// <returns>The size of the type in bytes.</returns>
101+
private static int GetFieldSize(string typeName) => typeName switch
102+
{
103+
"long" or "double" => 8,
104+
"int" or "float" => 4,
105+
"short" or "char" => 2,
106+
"bool" or "byte" => 1,
107+
_ => 8 // Conservative default
108+
};
109+
110+
/// <inheritdoc />
111+
public CommandResult Execute(params string[] args)
112+
{
113+
try
114+
{
115+
var results = AnalyzerExecutor.ExecutePath(this, args, "Usage: StructPadding <fileOrDirectoryPath>");
116+
var output = results.Count > 0
117+
? string.Join("\n", results.Select(d => d.ToString()))
118+
: "No padding issues found.";
119+
120+
return CommandResult.Ok(output, EnumTypes.Wstring);
121+
}
122+
catch (Exception ex)
123+
{
124+
return CommandResult.Fail(ex.Message);
125+
}
126+
}
127+
}
128+
}

Core.Apps/Rules/UnusedLocalVariableAnalyzer.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,7 @@ public IEnumerable<Diagnostic> Analyze(string filePath, string fileContent)
7070
if (symbol is not ILocalSymbol localSymbol)
7171
continue;
7272

73-
var references = root.DescendantNodes()
74-
.OfType<IdentifierNameSyntax>()
75-
.Where(id =>
76-
SymbolEqualityComparer.Default.Equals(model.GetSymbolInfo(id).Symbol, localSymbol));
77-
78-
if (references.Any())
73+
if (CoreHelper.IsSymbolUsed(model, root, localSymbol))
7974
continue;
8075

8176
var line = variable.GetLocation().GetLineSpan().StartLinePosition.Line + 1;

0 commit comments

Comments
 (0)