Skip to content

Commit 7875a89

Browse files
committed
Enforce approved terminology and retire protocol-only support
Add the implementation-terminology compiler error with explicit approved contexts. Remove obsolete protocol tests and restore wrappers, simplify native storage identity, and update copy acknowledgement checks.
1 parent c779934 commit 7875a89

736 files changed

Lines changed: 1051 additions & 35567 deletions

File tree

Some content is hidden

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

‎.editorconfig‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ dotnet_sort_system_directives_first = true
2121

2222
# Native Orleans checks resolved field IDs, including constants and partial models.
2323
dotnet_diagnostic.ORLEANS0012.severity = error
24+
dotnet_diagnostic.KLD0038.severity = error
2425

2526
# Don't use this. qualifier
2627
dotnet_style_qualification_for_field = false:warning
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
8+
namespace KeyLoad.Analyzers.Features.CodeQuality;
9+
10+
internal sealed class ImplementationTermPermissions(SemanticModel model)
11+
{
12+
private readonly Dictionary<(MemberDeclarationSyntax Owner, string Term), bool> references = new();
13+
14+
internal bool Allows(string term, string text, SyntaxNode node, CancellationToken cancellationToken)
15+
{
16+
var symbol = ImplementationTermSymbols.Resolve(model, node, cancellationToken);
17+
if (ImplementationTermSymbols.Native(symbol) || IsVocabulary(symbol))
18+
{
19+
return true;
20+
}
21+
var owner = node.AncestorsAndSelf().OfType<MemberDeclarationSyntax>().FirstOrDefault();
22+
var ownerSymbol = ImplementationTermSymbols.Resolve(model, owner, cancellationToken);
23+
if (IsVocabulary(ownerSymbol))
24+
{
25+
return true;
26+
}
27+
return term switch
28+
{
29+
ImplementationTermCatalog.MovementWord => NativeMovement(text),
30+
ImplementationTermCatalog.DataWord => ImplementationTermSymbols.TimeSeries(symbol, model.Compilation) ||
31+
ImplementationTermSymbols.TimeSeries(ownerSymbol, model.Compilation) ||
32+
References(owner, term, static (candidate, compilation) => ImplementationTermSymbols.TimeSeries(candidate, compilation), cancellationToken),
33+
ImplementationTermCatalog.ResourceWord => ImplementationTermSymbols.TestOwner(symbol) ||
34+
ImplementationTermSymbols.TestOwner(ownerSymbol) ||
35+
References(owner, term, static (candidate, _) => ImplementationTermSymbols.TestOwner(candidate), cancellationToken),
36+
_ => false
37+
};
38+
}
39+
40+
private static bool IsVocabulary(ISymbol? symbol) =>
41+
symbol?.ContainingType?.ToDisplayString() == ImplementationTermCatalog.RuleVocabularyType &&
42+
(symbol is IFieldSymbol { IsConst: true } ||
43+
symbol is IFieldSymbol { IsStatic: true, Name: nameof(ImplementationTermCatalog.DataContractTypes) } ||
44+
symbol is IMethodSymbol { Name: nameof(ImplementationTermCatalog.Read) or "Classify" or "Compound" });
45+
46+
private static bool NativeMovement(string text) =>
47+
text.Contains(ImplementationTermCatalog.OrleansWord, StringComparison.Ordinal) &&
48+
text.Contains(ImplementationTermCatalog.ActivationWord, StringComparison.OrdinalIgnoreCase);
49+
50+
private bool References(MemberDeclarationSyntax? owner, string term,
51+
Func<ISymbol?, Compilation, bool> accepts, CancellationToken cancellationToken)
52+
{
53+
if (owner is null)
54+
{
55+
return false;
56+
}
57+
var key = (owner, term);
58+
if (references.TryGetValue(key, out var allowed))
59+
{
60+
return allowed;
61+
}
62+
foreach (var name in owner.DescendantNodes().OfType<SimpleNameSyntax>())
63+
{
64+
cancellationToken.ThrowIfCancellationRequested();
65+
if (accepts(ImplementationTermSymbols.Resolve(model, name, cancellationToken), model.Compilation))
66+
{
67+
references.Add(key, true);
68+
return true;
69+
}
70+
}
71+
references.Add(key, false);
72+
return false;
73+
}
74+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
8+
namespace KeyLoad.Analyzers.Features.CodeQuality;
9+
10+
internal static class ImplementationTermSymbols
11+
{
12+
internal static ISymbol? Resolve(SemanticModel model, SyntaxNode? node, CancellationToken cancellationToken)
13+
{
14+
if (node is null)
15+
{
16+
return null;
17+
}
18+
if (node is SimpleNameSyntax or MemberAccessExpressionSyntax)
19+
{
20+
return model.GetSymbolInfo(node, cancellationToken).Symbol;
21+
}
22+
if (node is FieldDeclarationSyntax field && field.Declaration.Variables.Count == 1)
23+
{
24+
return model.GetDeclaredSymbol(field.Declaration.Variables[0], cancellationToken);
25+
}
26+
return node is MemberDeclarationSyntax or VariableDeclaratorSyntax or ParameterSyntax or TypeParameterSyntax
27+
? model.GetDeclaredSymbol(node, cancellationToken) : null;
28+
}
29+
30+
internal static bool Native(ISymbol? symbol) => symbol is not null &&
31+
symbol.Locations.All(static location => !location.IsInSource) &&
32+
ImplementationTermCatalog.NativeNamespace(symbol.ContainingNamespace?.ToDisplayString() ?? string.Empty);
33+
34+
internal static bool HasPath(ISymbol? symbol, string segment) => symbol is not null &&
35+
(symbol.DeclaringSyntaxReferences.Any(reference => PathContains(reference.SyntaxTree.FilePath, segment)) ||
36+
symbol.ContainingType is { DeclaringSyntaxReferences.Length: > 0 } owner &&
37+
owner.DeclaringSyntaxReferences.All(reference => PathContains(reference.SyntaxTree.FilePath, segment)));
38+
39+
internal static bool PathContains(string path, string segment) =>
40+
path.Replace('\\', '/').Contains(segment, StringComparison.Ordinal);
41+
42+
internal static ITypeSymbol? ValueType(ISymbol? symbol) => symbol switch
43+
{
44+
ILocalSymbol local => local.Type,
45+
IParameterSymbol parameter => parameter.Type,
46+
IFieldSymbol field => field.Type,
47+
IPropertySymbol property => property.Type,
48+
IMethodSymbol method => method.ReturnType,
49+
ITypeSymbol type => type,
50+
_ => null
51+
};
52+
53+
internal static bool TestOwner(ISymbol? symbol)
54+
{
55+
if (HasPath(symbol, ImplementationTermCatalog.TestPath) && HasPath(symbol, ImplementationTermCatalog.TestOwnerPath))
56+
{
57+
return true;
58+
}
59+
var owner = ValueType(symbol) ?? symbol?.ContainingType;
60+
return TypeMeans(owner as ITypeSymbol, static type =>
61+
HasPath(type, ImplementationTermCatalog.TestPath) && HasPath(type, ImplementationTermCatalog.TestOwnerPath),
62+
new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default)) ||
63+
TypeMeans(symbol?.ContainingType, static type =>
64+
HasPath(type, ImplementationTermCatalog.TestPath) && HasPath(type, ImplementationTermCatalog.TestOwnerPath),
65+
new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default));
66+
}
67+
68+
internal static bool TimeSeries(ISymbol? symbol, Compilation compilation)
69+
{
70+
if (HasPath(symbol, ImplementationTermCatalog.TimeSeriesPath) ||
71+
HasPath(ValueType(symbol), ImplementationTermCatalog.TimeSeriesPath) ||
72+
symbol?.ContainingType?.ToDisplayString() == ImplementationTermCatalog.SampleFieldsType ||
73+
symbol?.ToDisplayString() == ImplementationTermCatalog.SampleFieldsType)
74+
{
75+
return true;
76+
}
77+
if (DataType(ValueType(symbol)) || DataType(symbol?.ContainingType))
78+
{
79+
return true;
80+
}
81+
return symbol is not null && ImplementationTermCatalog.DataNamespaces.Any(prefix =>
82+
DataType(compilation.GetTypeByMetadataName(prefix + "." + symbol.Name)));
83+
}
84+
85+
private static bool DataType(ITypeSymbol? type) => TypeMeans(type, static candidate =>
86+
HasPath(candidate, ImplementationTermCatalog.TimeSeriesPath) ||
87+
DataIdentity(candidate), new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default));
88+
89+
private static bool DataIdentity(ITypeSymbol type) =>
90+
type.ContainingAssembly?.Name.StartsWith(ImplementationTermCatalog.NamespacePrefix, StringComparison.Ordinal) == true &&
91+
type.ContainingNamespace?.ToDisplayString().EndsWith(ImplementationTermCatalog.DataFeatureNamespaceSuffix, StringComparison.Ordinal) == true ||
92+
type.ContainingAssembly?.Name == ImplementationTermCatalog.DataAssembly &&
93+
type.ContainingNamespace?.ToDisplayString() == ImplementationTermCatalog.DataNamespace &&
94+
Array.Exists(ImplementationTermCatalog.DataContractTypes, name => name == type.Name);
95+
96+
private static bool TypeMeans(ITypeSymbol? type, Func<ITypeSymbol, bool> accepts, HashSet<ITypeSymbol> visited)
97+
{
98+
if (type is null || !visited.Add(type))
99+
{
100+
return false;
101+
}
102+
if (accepts(type))
103+
{
104+
return true;
105+
}
106+
if (type is IArrayTypeSymbol array)
107+
{
108+
return TypeMeans(array.ElementType, accepts, visited);
109+
}
110+
return type is INamedTypeSymbol named &&
111+
(named.TypeArguments.Any(argument => TypeMeans(argument, accepts, visited)) ||
112+
named.AllInterfaces.Where(NativeModelContract).SelectMany(static contract => contract.TypeArguments)
113+
.Any(argument => TypeMeans(argument, accepts, visited)));
114+
}
115+
116+
private static bool NativeModelContract(INamedTypeSymbol contract) =>
117+
Native(contract) && Array.Exists(ImplementationTermCatalog.NativeModelContracts,
118+
name => name == contract.ContainingNamespace.ToDisplayString() + "." + contract.MetadataName);
119+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System;
2+
using System.Collections.Immutable;
3+
using System.Linq;
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CSharp;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
9+
namespace KeyLoad.Analyzers.Features.CodeQuality;
10+
11+
/// <summary>Requires explicit approval for implementation terminology outside its approved meanings.</summary>
12+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
13+
public sealed class UnauthorizedImplementationTermAnalyzer : DiagnosticAnalyzer
14+
{
15+
/// <summary>Stable compiler and SARIF diagnostic identifier.</summary>
16+
public const string DiagnosticId = ImplementationTermCatalog.DiagnosticId;
17+
private static readonly DiagnosticDescriptor Rule = new(DiagnosticId, ImplementationTermCatalog.Title,
18+
ImplementationTermCatalog.Message, CodeQualityDiagnosticCategories.Architecture, DiagnosticSeverity.Error,
19+
isEnabledByDefault: true, description: ImplementationTermCatalog.Description);
20+
21+
/// <inheritdoc/>
22+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];
23+
24+
/// <inheritdoc/>
25+
public override void Initialize(AnalysisContext context)
26+
{
27+
ArgumentNullException.ThrowIfNull(context);
28+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
29+
context.EnableConcurrentExecution();
30+
context.RegisterCompilationStartAction(start =>
31+
{
32+
if (start.Compilation.AssemblyName?.StartsWith(ImplementationTermCatalog.NamespacePrefix, StringComparison.Ordinal) == true)
33+
{
34+
start.RegisterSyntaxTreeAction(tree => AnalyzeTree(tree, start.Compilation));
35+
}
36+
});
37+
}
38+
39+
private static void AnalyzeTree(SyntaxTreeAnalysisContext context, Compilation compilation)
40+
{
41+
var root = context.Tree.GetRoot(context.CancellationToken);
42+
var model = compilation.GetSemanticModel(context.Tree);
43+
var permissions = new ImplementationTermPermissions(model);
44+
foreach (var token in root.DescendantTokens())
45+
{
46+
context.CancellationToken.ThrowIfCancellationRequested();
47+
if (token.IsKind(SyntaxKind.IdentifierToken) || token.Parent is LiteralExpressionSyntax ||
48+
token.IsKind(SyntaxKind.InterpolatedStringTextToken))
49+
{
50+
AnalyzeText(context, permissions, token.Parent!, token.ValueText, token.GetLocation());
51+
}
52+
}
53+
foreach (var trivia in root.DescendantTrivia().Where(IsComment))
54+
{
55+
context.CancellationToken.ThrowIfCancellationRequested();
56+
AnalyzeText(context, permissions, trivia.Token.Parent ?? root, trivia.ToFullString(), trivia.GetLocation());
57+
}
58+
}
59+
60+
private static bool IsComment(SyntaxTrivia trivia) => trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) ||
61+
trivia.IsKind(SyntaxKind.MultiLineCommentTrivia) || trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) ||
62+
trivia.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia);
63+
64+
private static void AnalyzeText(SyntaxTreeAnalysisContext context, ImplementationTermPermissions permissions,
65+
SyntaxNode node, string text, Location location)
66+
{
67+
foreach (var match in ImplementationTermCatalog.Read(text).Distinct())
68+
{
69+
context.CancellationToken.ThrowIfCancellationRequested();
70+
if (!permissions.Allows(match.Term, text, node, context.CancellationToken))
71+
{
72+
context.ReportDiagnostic(Diagnostic.Create(Rule, location, match.Text));
73+
}
74+
}
75+
}
76+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text.RegularExpressions;
4+
5+
namespace KeyLoad.Analyzers.Features.CodeQuality;
6+
7+
internal static class ImplementationTermCatalog
8+
{
9+
internal const string DiagnosticId = "KLD0038";
10+
internal const string Title = "Implementation terminology requires explicit approval";
11+
internal const string Message = "Term '{0}' requires explicit owner approval in this implementation context";
12+
internal const string Description = "Only the explicitly approved native runtime, domain and testing meanings are permitted. Renaming an unauthorized mechanism does not authorize it.";
13+
internal const string NamespacePrefix = "KeyLoad.";
14+
internal const string RuleVocabularyType = "KeyLoad.Analyzers.Features.CodeQuality.ImplementationTermCatalog";
15+
internal const string TimeSeriesPath = "/TimeSeries/";
16+
internal const string TestPath = "/tests/";
17+
internal const string TestOwnerPath = "/Fixtures/";
18+
internal const string OrleansWord = "Orleans";
19+
internal const string ActivationWord = "activation";
20+
internal const string DataWord = "sample";
21+
internal const string ResourceWord = "fixture";
22+
internal const string MovementWord = "migration";
23+
internal const string AlternateWord = "fallback";
24+
internal const string PriorFailureWord = "regression";
25+
internal const string SubstituteWord = "mock";
26+
internal const string DemonstrationWord = "simple";
27+
internal const string OlderContractWord = "compatibility";
28+
internal const string EarlierWord = "backward";
29+
internal const string MatchingWord = "compatible";
30+
internal const string SampleFieldsType = "KeyLoad.Core.Features.InternalSerialization.SampleReadScopeFields";
31+
internal const string DataAssembly = "KeyLoad.Abstractions";
32+
internal const string DataNamespace = "KeyLoad";
33+
internal const string DataFeatureNamespaceSuffix = ".Features.TimeSeries";
34+
internal static readonly string[] NativeNamespaces =
35+
[
36+
"System", "Microsoft", "Orleans", "ManagedCode", "Tenray.ZoneTree", "ZoneTree", "TUnit", "Aspire"
37+
];
38+
internal static readonly string[] DataNamespaces =
39+
[
40+
"KeyLoad", "KeyLoad.Core.Features.TimeSeries", "KeyLoad.Clustering.Features.TimeSeries"
41+
];
42+
internal static readonly string[] DataContractTypes =
43+
[
44+
"SampleData", "SampleRecord", "AppendSamples", "ExpireSamples", "AggregateSampleWindowsRequest",
45+
"SampleAggregateWindow", "ReadSamplesRequest", "SampleRollupResult", "ReadSampleRollupRequest",
46+
"LatestSampleResult", "SampleRollupProtocol", "SampleChunkProtocol", "AggregateSamplesRequest",
47+
"SampleAggregate", "TimeSeriesReadDefaults", "OpenSampleChunkWindow", "TimeSeriesReadProtocol",
48+
"DropSampleRollup", "SealSampleChunkWindow", "SampleRollupBucket", "RefreshSampleRollup",
49+
"SampleRetentionStatus", "ReadSampleRetentionRequest", "MergeSampleChunkWindow",
50+
"ReadSampleChunkWindowRequest", "DropSampleChunkWindow", "SampleRetentionDefaults",
51+
"ReadLatestSampleRequest", "SampleAggregateWindowsResult", "SampleChunkWindowResult"
52+
];
53+
internal static readonly string[] NativeModelContracts =
54+
[
55+
"Orleans.Serialization.Codecs.IFieldCodec`1", "Orleans.Serialization.Cloning.IDeepCopier`1",
56+
"Orleans.Serialization.Serializers.IBaseCodec`1", "Orleans.Serialization.Activators.IActivator`1"
57+
];
58+
59+
private static readonly Regex Words = new("[A-Z]?[a-z]+|[A-Z]+(?![a-z])", RegexOptions.CultureInvariant);
60+
61+
internal static IEnumerable<(string Term, string Text)> Read(string text)
62+
{
63+
var previous = string.Empty;
64+
foreach (Match word in Words.Matches(text))
65+
{
66+
var value = word.Value.ToUpperInvariant();
67+
var term = Classify(value, previous);
68+
term ??= word.Value == value ? Compound(value) : null;
69+
if (term is not null)
70+
{
71+
yield return (term, word.Value);
72+
}
73+
previous = value;
74+
}
75+
}
76+
77+
private static string? Classify(string value, string previous) => value switch
78+
{
79+
"FALLBACK" or "FALLBACKS" => AlternateWord,
80+
"BACK" when previous == "FALL" => AlternateWord,
81+
"MIGRATE" or "MIGRATES" or "MIGRATED" or "MIGRATING" or "MIGRATION" or "MIGRATIONS" => MovementWord,
82+
"REGRESS" or "REGRESSES" or "REGRESSED" or "REGRESSING" or "REGRESSION" or "REGRESSIONS" => PriorFailureWord,
83+
"MOCK" or "MOCKS" or "MOCKED" or "MOCKING" => SubstituteWord,
84+
"FIXTURE" or "FIXTURES" => ResourceWord,
85+
"SAMPLE" or "SAMPLES" or "SAMPLED" or "SAMPLING" => DataWord,
86+
"SIMPLE" => DemonstrationWord,
87+
"COMPATIBILITY" or "COMPATIBILITIES" => OlderContractWord,
88+
"COMPATIBLE" when previous is "BACKWARD" or "BACKWARDS" => OlderContractWord,
89+
_ => null
90+
};
91+
92+
private static string? Compound(string value) => value switch
93+
{
94+
_ when value.Contains("FALLBACK", StringComparison.Ordinal) => AlternateWord,
95+
_ when value.Contains("MIGRAT", StringComparison.Ordinal) => MovementWord,
96+
_ when value.Contains("REGRESS", StringComparison.Ordinal) => PriorFailureWord,
97+
_ when value.Contains("MOCK", StringComparison.Ordinal) => SubstituteWord,
98+
_ when value.Contains("FIXTURE", StringComparison.Ordinal) => ResourceWord,
99+
_ when value.Contains("SAMPLE", StringComparison.Ordinal) || value.Contains("SAMPLING", StringComparison.Ordinal) => DataWord,
100+
_ when value.Contains("SIMPLE", StringComparison.Ordinal) => DemonstrationWord,
101+
_ when value.Contains("COMPATIBILITY", StringComparison.Ordinal) || value.Contains("BACKWARDCOMPATIBLE", StringComparison.Ordinal) ||
102+
value.Contains("BACKWARDSCOMPATIBLE", StringComparison.Ordinal) => OlderContractWord,
103+
_ => null
104+
};
105+
106+
internal static bool NativeNamespace(string value) => Array.Exists(NativeNamespaces,
107+
prefix => value == prefix || value.StartsWith(prefix + ".", StringComparison.Ordinal));
108+
}

0 commit comments

Comments
 (0)