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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
16 changes: 16 additions & 0 deletions .csharpierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# SBE-generated codecs and Fody weaver outputs are generated; CSharpier must
# not touch them. (CSharpier respects this regardless of any
# `<auto-generated>` markers; we list explicitly for safety.)
**/Codecs/
src/Weavers/

# Build outputs
bin/
obj/

# CSharpier 1.2+ formats MSBuild files by default. We don't want csproj/
# props/targets reformatted (would conflict with the existing 4-space
# indent). Exclude them.
*.csproj
*.props
*.targets
6 changes: 6 additions & 0 deletions .csharpierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"printWidth": 120,
"useTabs": false,
"tabWidth": 4,
"endOfLine": "lf"
}
371 changes: 371 additions & 0 deletions .editorconfig

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<Project>
<PropertyGroup>
<!-- Enforce .editorconfig style and naming rules during dotnet build, not just in the IDE.
Without this, IDE0xxx and IDE1006 diagnostics are silent at build time. -->
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>

<!-- IDE0005 (unused usings) only fires at build when an XML doc file is being produced.
Cheapest way to satisfy that without forcing the doc-comment warnings on every
public symbol is to emit the file and suppress the doc-coverage / cref warnings.
CS1591 — missing XML comment for publicly visible type or member
CS1584 — XML comment has syntactically incorrect cref attribute
CS1658 — overrides an existing warning (fires alongside CS1584) -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591;CS1584;CS1658</NoWarn>
</PropertyGroup>

<!-- Repo-wide analyzer packs. PrivateAssets=all keeps them out of the package graph
of any consumer that references our shipped libraries. -->
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.4.0.108396">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<!-- Local custom analyzer. Wired as an Analyzer-only ProjectReference so the
output DLL is loaded by Roslyn at compile time without becoming part of
the consumer's reference closure. Skipped on the analyzer project itself
to avoid a self-cycle and on Fody weavers which run in a different host. -->
<ItemGroup Condition="'$(MSBuildProjectName)' != 'Adaptive.Aeron.Analyzers' AND '$(MSBuildProjectName)' != 'Unsealer.Fody'">
<ProjectReference Include="$(MSBuildThisFileDirectory)src/Adaptive.Aeron.Analyzers/Adaptive.Aeron.Analyzers.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

<!-- SonarAnalyzer parameterised-rule config. Standalone NuGet ignores
`sonar.cs.SXXX.<param>` from .editorconfig (SonarCloud/SonarQube only)
but honours rule parameters supplied through SonarLint.xml when wired
in as a Roslyn AdditionalFiles input. -->
<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)SonarLint.xml"
Link="Properties\SonarLint.xml"
Visible="false" />
</ItemGroup>
</Project>
67 changes: 67 additions & 0 deletions SonarLint.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SonarAnalyzer.CSharp parameterised-rule configuration.

The standalone NuGet does not honour `sonar.cs.S138.max` from .editorconfig
(only SonarCloud/SonarQube do). This file is wired in via <AdditionalFiles>
in Directory.Build.props and IS honoured by the analyzer.

Threshold for S138 is set to 100 lines for parity with upstream Java
Aeron's Checkstyle MethodLength rule (config/checkstyle/checkstyle.xml).
-->
<AnalysisInput xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Settings>
<!-- SBE codecs and Fody weaver outputs are generated; Sonar's auto-detection
only matches `<auto-generated>` style headers, not the `/* Generated SBE */`
comment our codecs ship with. This switch disables Sonar analysis on any
file Roslyn flags as generated, which the .editorconfig
`generated_code = true` blocks under [**/Codecs/**.cs] mark for us. -->
<Setting>
<Key>sonar.cs.analyzeGeneratedCode</Key>
<Value>false</Value>
</Setting>
</Settings>
<Rules>
<!-- S138 method length, parity with Checkstyle MethodLength (max=100). -->
<Rule>
<Key>S138</Key>
<Parameters>
<Parameter>
<Key>max</Key>
<Value>100</Value>
</Parameter>
</Parameters>
</Rule>

<!-- S103 line length, parity with Checkstyle LineLength (max=120). -->
<Rule>
<Key>S103</Key>
<Parameters>
<Parameter>
<Key>maximumLineLength</Key>
<Value>120</Value>
</Parameter>
</Parameters>
</Rule>

<!-- S1451 copyright header, parity with Checkstyle RegexpHeader. The
regex permits files copyrighted to either Adaptive Financial
Consulting (Ltd / Limited) or Real Logic, with an optional year
range. Files missing the header entirely are flagged. -->
<Rule>
<Key>S1451</Key>
<Parameters>
<Parameter>
<Key>headerFormat</Key>
<Value>/\*
\* Copyright \d{4}( ?- ?\d{4})? (Adaptive Financial Consulting|Real Logic) (Ltd|Limited)\.?</Value>
</Parameter>
<Parameter>
<Key>isRegularExpression</Key>
<Value>true</Value>
</Parameter>
</Parameters>
</Rule>
</Rules>
</AnalysisInput>
21 changes: 21 additions & 0 deletions src/Adaptive.Aeron.Analyzers/Adaptive.Aeron.Analyzers.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
<IncludeBuildOutput>false</IncludeBuildOutput>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>
</Project>
2 changes: 2 additions & 0 deletions src/Adaptive.Aeron.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
; Shipped analyzer releases.
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
8 changes: 8 additions & 0 deletions src/Adaptive.Aeron.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
; Unshipped analyzer release.
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
AERON0002 | Naming | Warning | MS PascalCase acronym detector. Catches 3+ letter all-caps acronyms (XMLParser -> XmlParser) and the MS-special-cased ID (StreamID -> StreamId). Allows 2-letter acronyms (IO, DB, UI) per MS conventions.
188 changes: 188 additions & 0 deletions src/Adaptive.Aeron.Analyzers/StrictPascalCaseAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright 2014 - 2026 Adaptive Financial Consulting Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Adaptive.Aeron.Analyzers;

/// <summary>
/// Flags identifiers that violate the Microsoft .NET capitalization conventions
/// (https://learn.microsoft.com/dotnet/standard/design-guidelines/capitalization-conventions).
///
/// Rules enforced:
/// 1. Acronyms of 3+ letters must be PascalCase (Xml, Sql, Html), not all-caps.
/// 2. The abbreviation "ID" is treated as a word ("Id"), even though it's only
/// 2 letters — this is an explicit MS carve-out.
///
/// Not enforced (per MS): 2-letter acronyms stay all-caps (IO, DB, UI, OK).
/// Roslyn's first-char-only pascal_case check misses both rules; this analyzer
/// closes the gap for non-API symbols (plus public/protected in test paths).
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class StrictPascalCaseAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "AERON0002";

private static readonly DiagnosticDescriptor Rule = new(
DiagnosticId,
title: "Identifier violates MS PascalCase capitalization rules",
messageFormat:
"Identifier '{0}' contains a {1}-character all-caps acronym; only " +
"2-letter acronyms stay uppercase per MS conventions, and 'ID' is " +
"cased as 'Id'",
category: "Naming",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description:
"Microsoft's .NET capitalization conventions require 3+ letter acronyms " +
"to be PascalCase (Xml, Sql, Html). 2-letter acronyms stay all-caps " +
"(IO, DB, UI). The abbreviation 'ID' is a special case treated as a " +
"word ('Id'). Roslyn's built-in pascal_case rule only checks the first " +
"character, so this analyzer closes the gap for non-API symbols.");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSymbolAction(
AnalyzeSymbol,
SymbolKind.Field,
SymbolKind.Method,
SymbolKind.Property,
SymbolKind.Event,
SymbolKind.NamedType,
SymbolKind.Parameter);
}

private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
var symbol = context.Symbol;
var name = symbol.Name;
if (!TryFindViolation(name, out var acronymLen))
{
return;
}

if (symbol.Locations.Length == 0)
{
return;
}

var path = symbol.Locations[0].SourceTree?.FilePath ?? "";

// Skip SBE codecs (auto-generated; Roslyn's generated_code=true does
// not propagate to third-party analyzers).
if (path.Contains("/Codecs/") || path.Contains("\\Codecs\\"))
{
return;
}

// Non-API only — EXCEPT in test code, where public/protected aren't
// consumer API and aren't subject to Java upstream parity.
if (!IsTestPath(path))
{
switch (symbol.DeclaredAccessibility)
{
case Accessibility.Private:
case Accessibility.Internal:
case Accessibility.ProtectedOrInternal:
case Accessibility.ProtectedAndInternal:
break;
default:
return;
}
}

context.ReportDiagnostic(Diagnostic.Create(
Rule, symbol.Locations[0], name, acronymLen));
}

/// <summary>
/// Returns true if <paramref name="name"/> contains either:
/// • A run of consecutive uppercase letters that decomposes into a
/// 3+-letter acronym (with optional trailing PascalCase word start), or
/// • The substring "ID" positioned as a standalone 2-letter acronym
/// (MS-special-cased to "Id").
/// </summary>
private static bool TryFindViolation(string name, out int acronymLen)
{
acronymLen = 0;

// Check 1: 3+ letter acronyms.
for (int i = 0; i < name.Length;)
{
if (!char.IsUpper(name[i]))
{
i++;
continue;
}

int j = i;
while (j < name.Length && char.IsUpper(name[j]))
{
j++;
}

int runLen = j - i;
if (runLen >= 3)
{
// If the run is followed by a lowercase letter, the last
// uppercase letter is the first character of a new PascalCase
// word — so the acronym proper is (runLen - 1) letters long.
// Otherwise the whole run is an acronym.
bool lastStartsNewWord = j < name.Length && char.IsLower(name[j]);
int actualAcronymLen = lastStartsNewWord ? runLen - 1 : runLen;
if (actualAcronymLen >= 3)
{
acronymLen = actualAcronymLen;
return true;
}
}

i = j;
}

// Check 2: "ID" as a standalone 2-letter acronym (the MS special case).
for (int i = 0; i + 1 < name.Length; i++)
{
if (name[i] != 'I' || name[i + 1] != 'D')
{
continue;
}

bool leftOk = i == 0 || char.IsLower(name[i - 1]);
bool rightOk = i + 2 == name.Length
|| char.IsUpper(name[i + 2])
|| char.IsDigit(name[i + 2]);
if (leftOk && rightOk)
{
acronymLen = 2;
return true;
}
}

return false;
}

private static bool IsTestPath(string path) =>
path.Contains(".Tests/") || path.Contains(".Tests\\") ||
path.Contains(".Test/") || path.Contains(".Test\\");
}
6 changes: 6 additions & 0 deletions src/Adaptive.Aeron.Tests/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Test method names follow the Method_Scenario_Result convention by design.
# Disable the non-API method PascalCase rule for this project; locals/params/
# fields still follow the root .editorconfig rules.

[*.cs]
dotnet_naming_rule.non_api_methods_must_be_pascal.severity = none
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
extensions: designer.cs generated.cs
extensions: .cs .cpp .h
/*
* Copyright 2014 - 2017 Adaptive Financial Consulting Ltd
* Copyright 2014 - 2026 Adaptive Financial Consulting Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
Loading
Loading