Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/architecture/IMPLEMENTATION_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,26 @@ Decisions and gotchas:
- `JsonSchema.Net` is referenced by `CodeGuard.Core.Tests` only, to validate output. Each schema is
parsed once in a static `Lazy`, because `JsonSchema.FromText` throws on a duplicate `$id`.

### Post-v1 addition: violation end positions and metadata (#68)

`Violation` (and `AnalyzerViolation`) gained optional `EndLine`/`EndColumn` (`int?`, default `null`)
and `Violation` gained `Metadata` (`IReadOnlyDictionary<string, string>?`, default `null`). Existing
constructors and call sites still compile.

- End positions come from `GetLineSpan().EndLinePosition`, using the same 1-based convention as the
start. The end column is exclusive, the column just past the last character, which matches SARIF
`region.endColumn`. They are threaded through the Roslyn-backed models (`TypeModel`, `MethodModel`,
`PropertyModel`, `ConstructorModel`, `FieldModel`, `CallSiteModel`, `DiagnosticModel`, and the syntax
fact models `SwitchModel`, `ThrowSiteModel`, `TryBlockModel`, `MethodBodyShapeModel`,
`MutationSiteModel`) and into the analyzers that produce violations from them.
- Span coverage: type and member end positions cover the declaring identifier, because that is the
symbol location. Syntax fact models carry only `EndLine`/`EndColumn`, not a start column. Repository,
project, file and MSBuild-derived violations have no end span.
- SARIF: `Region.EndLine`/`EndColumn` are set only when present. `Metadata` entries go into the result's
property bag under their own keys.
- JSON (v1 contract): `endLine`, `endColumn` and `metadata` are optional additive properties on each
violation, documented in `RESULT_CONTRACT.md` and the schema. No built-in rule populates `Metadata` yet.

## The 11 starter rules

All under `rules/`, all illustrative (`Contoso.*` namespace, `illustrative: true`), matching the
Expand Down
10 changes: 8 additions & 2 deletions docs/architecture/RESULT_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,14 @@ object has `additionalProperties: false`, so a consumer can rely on the set of k
}
```

Optional fields (`file`, `line`, `column`, `symbol`, `project`, `remediation`, and the optional
fields of `evaluationErrors` and `analysisWarnings`) are omitted when unknown, not written as `null`.
Optional fields (`file`, `line`, `column`, `endLine`, `endColumn`, `metadata`, `symbol`, `project`,
`remediation`, and the optional fields of `evaluationErrors` and `analysisWarnings`) are omitted when
unknown, not written as `null`.

Per violation, `endLine` and `endColumn` give the end of the source span when the producer knows it
(currently Roslyn-backed rules). `endColumn` is 1-based and exclusive, i.e. the column just past the last
character, matching SARIF `region.endColumn`. `metadata` is an optional string-to-string bag for
producer-specific details; no built-in rule populates it yet.

## Top-level fields

Expand Down
4 changes: 3 additions & 1 deletion src/CodeGuard.Analysis/AnalysisModel/CallSiteModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ public sealed record CallSiteModel(
int Column,
string? EnclosingComparisonOperator = null,
string? EnclosingComparisonValue = null,
string ProjectPath = "");
string ProjectPath = "",
int? EndLine = null,
int? EndColumn = null);
4 changes: 3 additions & 1 deletion src/CodeGuard.Analysis/AnalysisModel/DiagnosticModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ public sealed record DiagnosticModel(
string ProjectName,
string FilePath,
int Line,
int Column);
int Column,
int? EndLine = null,
int? EndColumn = null);
16 changes: 12 additions & 4 deletions src/CodeGuard.Analysis/AnalysisModel/MemberModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ public sealed record MethodModel(
string ProjectName,
string FilePath,
int Line,
int Column);
int Column,
int? EndLine = null,
int? EndColumn = null);

public sealed record PropertyModel(
string Name,
Expand All @@ -28,7 +30,9 @@ public sealed record PropertyModel(
string ProjectName,
string FilePath,
int Line,
int Column);
int Column,
int? EndLine = null,
int? EndColumn = null);

public sealed record ConstructorModel(
Accessibility Accessibility,
Expand All @@ -38,7 +42,9 @@ public sealed record ConstructorModel(
string ProjectName,
string FilePath,
int Line,
int Column);
int Column,
int? EndLine = null,
int? EndColumn = null);

public sealed record FieldModel(
string Name,
Expand All @@ -51,7 +57,9 @@ public sealed record FieldModel(
string FilePath,
int Line,
int Column,
string? ConstantValue = null);
string? ConstantValue = null,
int? EndLine = null,
int? EndColumn = null);

public sealed record ParameterModel(
string Name,
Expand Down
20 changes: 15 additions & 5 deletions src/CodeGuard.Analysis/AnalysisModel/SyntaxFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ public sealed record SwitchModel(
IReadOnlyList<string> ArmLabels,
bool HasDefaultOrDiscardArm,
string FilePath,
int Line);
int Line,
int? EndLine = null,
int? EndColumn = null);

public sealed record ThrowSiteModel(
string ContainingMethod,
Expand All @@ -16,7 +18,9 @@ public sealed record ThrowSiteModel(
string? ExceptionTypeName,
bool IsFirstStatementInMethod,
string FilePath,
int Line);
int Line,
int? EndLine = null,
int? EndColumn = null);

public sealed record MutationSiteModel(
string ContainingMethod,
Expand All @@ -25,7 +29,9 @@ public sealed record MutationSiteModel(
string ProjectName,
string FilePath,
int Line,
string ProjectPath = "");
string ProjectPath = "",
int? EndLine = null,
int? EndColumn = null);

public sealed record TryBlockModel(
string ContainingMethod,
Expand All @@ -34,7 +40,9 @@ public sealed record TryBlockModel(
int CatchClauseCount,
IReadOnlyList<string> CatchTypeNames,
string FilePath,
int Line);
int Line,
int? EndLine = null,
int? EndColumn = null);

public sealed record MethodBodyShapeModel(
string ContainingMethod,
Expand All @@ -44,4 +52,6 @@ public sealed record MethodBodyShapeModel(
bool IsSingleBaseCallDelegation,
string FilePath,
int Line,
string ProjectPath = "");
string ProjectPath = "",
int? EndLine = null,
int? EndColumn = null);
4 changes: 3 additions & 1 deletion src/CodeGuard.Analysis/AnalysisModel/TypeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ public sealed record TypeModel(
string FilePath,
int Line,
int Column,
string ProjectPath = "");
string ProjectPath = "",
int? EndLine = null,
int? EndColumn = null);

public enum TypeKind
{
Expand Down
4 changes: 3 additions & 1 deletion src/CodeGuard.Analyzers.Roslyn/RoslynDiagnosticExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ public static IReadOnlyList<DiagnosticModel> Extract(CSharpCompilation compilati
projectName,
lineSpan.Path,
lineSpan.StartLinePosition.Line + 1,
lineSpan.StartLinePosition.Character + 1);
lineSpan.StartLinePosition.Character + 1,
lineSpan.EndLinePosition.Line + 1,
lineSpan.EndLinePosition.Character + 1);
})
.ToList();
}
34 changes: 24 additions & 10 deletions src/CodeGuard.Analyzers.Roslyn/RoslynTypeExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ private static TypeModel ToTypeModel(INamedTypeSymbol symbol, string projectName
FilePath: location?.SourceTree?.FilePath ?? string.Empty,
Line: (lineSpan?.StartLinePosition.Line ?? -1) + 1,
Column: (lineSpan?.StartLinePosition.Character ?? -1) + 1,
ProjectPath: projectPath);
ProjectPath: projectPath,
EndLine: lineSpan?.EndLinePosition.Line + 1,
EndColumn: lineSpan?.EndLinePosition.Character + 1);
}

private static TypeKind MapTypeKind(INamedTypeSymbol symbol) => symbol.TypeKind switch
Expand Down Expand Up @@ -131,19 +133,23 @@ private static bool IsPartial(INamedTypeSymbol symbol) =>
_ => constant.Value?.ToString() ?? "null"
};

private static (string FilePath, int Line, int Column) GetLocation(ISymbol symbol)
// End positions use the same 1-based convention as the start: Roslyn's EndLinePosition is the
// exclusive end of the span, so +1 yields the column just past the last character (SARIF endColumn semantics).
private static (string FilePath, int Line, int Column, int? EndLine, int? EndColumn) GetLocation(ISymbol symbol)
{
var location = symbol.Locations.FirstOrDefault(l => l.IsInSource);
var lineSpan = location?.GetLineSpan();
return (
location?.SourceTree?.FilePath ?? string.Empty,
(lineSpan?.StartLinePosition.Line ?? -1) + 1,
(lineSpan?.StartLinePosition.Character ?? -1) + 1);
(lineSpan?.StartLinePosition.Character ?? -1) + 1,
lineSpan?.EndLinePosition.Line + 1,
lineSpan?.EndLinePosition.Character + 1);
}

private static MethodModel ToMethodModel(IMethodSymbol method, string declaringType, string projectName)
{
var (filePath, line, column) = GetLocation(method);
var (filePath, line, column, endLine, endColumn) = GetLocation(method);
return new MethodModel(
Name: method.Name,
ReturnType: method.ReturnType.ToDisplayString(),
Expand All @@ -155,7 +161,9 @@ private static MethodModel ToMethodModel(IMethodSymbol method, string declaringT
ProjectName: projectName,
FilePath: filePath,
Line: line,
Column: column);
Column: column,
EndLine: endLine,
EndColumn: endColumn);
}

private static MethodModifiers MapMethodModifiers(IMethodSymbol method)
Expand All @@ -171,7 +179,7 @@ private static MethodModifiers MapMethodModifiers(IMethodSymbol method)

private static PropertyModel ToPropertyModel(IPropertySymbol property, string declaringType, string projectName)
{
var (filePath, line, column) = GetLocation(property);
var (filePath, line, column, endLine, endColumn) = GetLocation(property);
return new PropertyModel(
Name: property.Name,
Type: property.Type.ToDisplayString(),
Expand All @@ -187,12 +195,14 @@ private static PropertyModel ToPropertyModel(IPropertySymbol property, string de
ProjectName: projectName,
FilePath: filePath,
Line: line,
Column: column);
Column: column,
EndLine: endLine,
EndColumn: endColumn);
}

private static ConstructorModel ToConstructorModel(IMethodSymbol constructor, string declaringType, string projectName)
{
var (filePath, line, column) = GetLocation(constructor);
var (filePath, line, column, endLine, endColumn) = GetLocation(constructor);
return new ConstructorModel(
Accessibility: MapAccessibility(constructor.DeclaredAccessibility),
Parameters: constructor.Parameters.Select(ToParameterModel).ToList(),
Expand All @@ -201,12 +211,14 @@ private static ConstructorModel ToConstructorModel(IMethodSymbol constructor, st
ProjectName: projectName,
FilePath: filePath,
Line: line,
Column: column);
Column: column,
EndLine: endLine,
EndColumn: endColumn);
}

private static FieldModel ToFieldModel(IFieldSymbol field, string declaringType, string projectName)
{
var (filePath, line, column) = GetLocation(field);
var (filePath, line, column, endLine, endColumn) = GetLocation(field);
return new FieldModel(
Name: field.Name,
Type: field.Type.ToDisplayString(),
Expand All @@ -218,6 +230,8 @@ private static FieldModel ToFieldModel(IFieldSymbol field, string declaringType,
FilePath: filePath,
Line: line,
Column: column,
EndLine: endLine,
EndColumn: endColumn,
ConstantValue: field.HasConstantValue ? field.ConstantValue?.ToString() : null);
}

Expand Down
29 changes: 17 additions & 12 deletions src/CodeGuard.Analyzers.Roslyn/SyntaxFactWalker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ private void RecordCallSite(SyntaxNode node, CallSiteKind kind, SeparatedSyntaxL
})
.ToList();

var (containingMethod, containingType, filePath, line, column) = DescribeLocation(node);
var (containingMethod, containingType, filePath, line, column, endLine, endColumn) = DescribeLocation(node);
var (comparisonOperator, comparisonValue) = kind == CallSiteKind.Invocation
? TryGetEnclosingComparison(node)
: (null, null);
Expand All @@ -135,6 +135,8 @@ private void RecordCallSite(SyntaxNode node, CallSiteKind kind, SeparatedSyntaxL
FilePath: filePath,
Line: line,
Column: column,
EndLine: endLine,
EndColumn: endColumn,
EnclosingComparisonOperator: comparisonOperator,
EnclosingComparisonValue: comparisonValue,
ProjectPath: projectPath));
Expand All @@ -156,28 +158,28 @@ private void RecordCallSite(SyntaxNode node, CallSiteKind kind, SeparatedSyntaxL

private void RecordSwitch(SyntaxNode node, IReadOnlyList<string> armLabels, bool hasDefaultOrDiscard)
{
var (containingMethod, containingType, filePath, line, _) = DescribeLocation(node);
sink.AddSwitch(new SwitchModel(containingMethod, containingType, projectName, armLabels, hasDefaultOrDiscard, filePath, line));
var (containingMethod, containingType, filePath, line, _, endLine, endColumn) = DescribeLocation(node);
sink.AddSwitch(new SwitchModel(containingMethod, containingType, projectName, armLabels, hasDefaultOrDiscard, filePath, line, endLine, endColumn));
}

private void RecordThrowSite(SyntaxNode node, ExpressionSyntax? expression)
{
var exceptionTypeName = expression is not null ? semanticModel.GetTypeInfo(expression).Type?.ToDisplayString() : null;
var isFirst = IsFirstStatementInEnclosingBlock(node);
var (containingMethod, containingType, filePath, line, _) = DescribeLocation(node);
sink.AddThrowSite(new ThrowSiteModel(containingMethod, containingType, projectName, exceptionTypeName, isFirst, filePath, line));
var (containingMethod, containingType, filePath, line, _, endLine, endColumn) = DescribeLocation(node);
sink.AddThrowSite(new ThrowSiteModel(containingMethod, containingType, projectName, exceptionTypeName, isFirst, filePath, line, endLine, endColumn));
}

private void RecordMutationSite(SyntaxNode node, string targetMemberName)
{
var (containingMethod, containingType, filePath, line, _) = DescribeLocation(node);
sink.AddMutationSite(new MutationSiteModel(containingMethod, containingType, targetMemberName, projectName, filePath, line, projectPath));
var (containingMethod, containingType, filePath, line, _, endLine, endColumn) = DescribeLocation(node);
sink.AddMutationSite(new MutationSiteModel(containingMethod, containingType, targetMemberName, projectName, filePath, line, projectPath, endLine, endColumn));
}

private void RecordTryBlock(SyntaxNode node, int catchClauseCount, IReadOnlyList<string> catchTypeNames)
{
var (containingMethod, containingType, filePath, line, _) = DescribeLocation(node);
sink.AddTryBlock(new TryBlockModel(containingMethod, containingType, projectName, catchClauseCount, catchTypeNames, filePath, line));
var (containingMethod, containingType, filePath, line, _, endLine, endColumn) = DescribeLocation(node);
sink.AddTryBlock(new TryBlockModel(containingMethod, containingType, projectName, catchClauseCount, catchTypeNames, filePath, line, endLine, endColumn));
}

private void RecordMethodBodyShape(MethodDeclarationSyntax node)
Expand Down Expand Up @@ -208,7 +210,8 @@ private void RecordMethodBodyShape(MethodDeclarationSyntax node)

sink.AddMethodBodyShape(new MethodBodyShapeModel(
node.Identifier.Text, containingType, projectName, statementCount, isSingleBaseCallDelegation,
location.SourceTree?.FilePath ?? string.Empty, lineSpan.StartLinePosition.Line + 1, projectPath));
location.SourceTree?.FilePath ?? string.Empty, lineSpan.StartLinePosition.Line + 1, projectPath,
lineSpan.EndLinePosition.Line + 1, lineSpan.EndLinePosition.Character + 1));
}

private static bool IsBaseDelegationStatement(StatementSyntax statement, string methodName) => statement switch
Expand All @@ -234,7 +237,7 @@ private static bool IsFirstStatementInEnclosingBlock(SyntaxNode node)
return statement?.Parent is not BlockSyntax block || block.Statements.FirstOrDefault() == statement;
}

private (string ContainingMethod, string ContainingType, string FilePath, int Line, int Column) DescribeLocation(SyntaxNode node)
private (string ContainingMethod, string ContainingType, string FilePath, int Line, int Column, int? EndLine, int? EndColumn) DescribeLocation(SyntaxNode node)
{
var enclosingSymbol = semanticModel.GetEnclosingSymbol(node.SpanStart);
var containingMethod = (enclosingSymbol as IMethodSymbol)?.Name ?? enclosingSymbol?.Name ?? "<unknown>";
Expand All @@ -248,7 +251,9 @@ private static bool IsFirstStatementInEnclosingBlock(SyntaxNode node)
containingType,
location.SourceTree?.FilePath ?? string.Empty,
lineSpan.StartLinePosition.Line + 1,
lineSpan.StartLinePosition.Character + 1);
lineSpan.StartLinePosition.Character + 1,
lineSpan.EndLinePosition.Line + 1,
lineSpan.EndLinePosition.Character + 1);
}

private static string DescribeLabel(SwitchLabelSyntax label) => label switch
Expand Down
Loading
Loading