diff --git a/.editorconfig b/.editorconfig index e6735ee..4fda749 100644 --- a/.editorconfig +++ b/.editorconfig @@ -27,8 +27,10 @@ dotnet_search_reference_assemblies = true # Nullability settings dotnet_build_property.Nullable = enable -# Enable or disable the analyzers -dotnet_analyzer_diagnostic.severity = warning +# Per-rule severity is authoritative in this file. The SDK sets AnalysisMode/AnalysisLevel as +# MSBuild properties, and the .NET SDK ignores bulk 'dotnet_analyzer_diagnostic.*' severity +# configuration whenever those properties are present. Do not re-add a bulk entry here: it +# silently does nothing and hides which severities are actually enforced. # Visual Studio XML Project Files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] @@ -76,23 +78,20 @@ indent_style = tab dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = warning dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields -dotnet_naming_rule.private_fields.severity = warning -dotnet_naming_rule.private_fields.style = camel_case_underscore -dotnet_naming_rule.private_fields.symbols = private_fields -dotnet_naming_rule.private_fields_style.severity = warning -dotnet_naming_rule.private_fields_style.style = camel_case -dotnet_naming_rule.private_fields_style.symbols = private_fields dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case +# NOTE: the private-field rules that used to sit here referenced a style ('camel_case_underscore') +# and a symbol group ('private_fields') that were never defined, so they never applied. The real +# private-field rules are declared in the "StyleCop Field Naming Rules" section below. dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field dotnet_naming_symbols.non_private_static_fields.required_modifiers = static -# Constants are PascalCase +# Constants are PascalCase (field constants; local constants are locals and stay camelCase) dotnet_naming_rule.constants_should_be_pascal_case.severity = warning dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants dotnet_naming_style.constant_style.capitalization = pascal_case -dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.applicable_kinds = field dotnet_naming_symbols.constants.required_modifiers = const # Locals and parameters are camelCase @@ -102,9 +101,7 @@ dotnet_naming_rule.locals_should_be_camel_case.severity = warning # camel_case_style - Define the camelCase style dotnet_naming_style.camel_case_style.capitalization = camel_case -dotnet_naming_style.static_field_style.required_prefix = s_ dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local -dotnet_naming_symbols.static_fields.required_modifiers = static # first_upper_style - The first character must start with an upper-case character dotnet_naming_style.first_upper_style.capitalization = first_word_upper @@ -153,9 +150,6 @@ dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = fie # StyleCop Field Naming Rules # All constant fields must be PascalCase -dotnet_naming_rule.private_or_internal_field_should_be__fieldname.severity = warning -dotnet_naming_rule.private_or_internal_field_should_be__fieldname.style = _fieldname -dotnet_naming_rule.private_or_internal_field_should_be__fieldname.symbols = private_or_internal_field dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = non_private_static_field_style dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group @@ -178,12 +172,22 @@ dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = style dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field -# Private fields must be camelCase -dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = warning -dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = camel_case_style -dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_private_fields_group -dotnet_naming_symbols.stylecop_private_fields_group.applicable_accessibilities = private -dotnet_naming_symbols.stylecop_private_fields_group.applicable_kinds = field +# Private static fields are PascalCase: they are type-level state and are never qualified with 'this.' +dotnet_naming_rule.private_static_fields_must_be_pascal_case_rule.severity = warning +dotnet_naming_rule.private_static_fields_must_be_pascal_case_rule.style = non_private_static_field_style +dotnet_naming_rule.private_static_fields_must_be_pascal_case_rule.symbols = private_static_fields_group +dotnet_naming_symbols.private_static_fields_group.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_group.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_group.required_modifiers = static + +# Private instance fields must be camelCase with a leading underscore: '_name', never 'name'. The +# prefix keeps field access unambiguous (so the noisy 'this.' qualifier is never needed) and keeps +# fields distinguishable from locals and parameters. +dotnet_naming_rule.private_instance_fields_must_be_camel_case_with_underscore_prefix.severity = warning +dotnet_naming_rule.private_instance_fields_must_be_camel_case_with_underscore_prefix.style = camel_case_underscore_style +dotnet_naming_rule.private_instance_fields_must_be_camel_case_with_underscore_prefix.symbols = private_instance_fields_group +dotnet_naming_symbols.private_instance_fields_group.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_group.applicable_kinds = field # Local variables must be camelCase dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent @@ -232,25 +236,14 @@ dotnet_naming_style.type_parameter_style.required_prefix = T dotnet_naming_symbols.type_parameter_symbol.applicable_accessibilities = * dotnet_naming_symbols.type_parameter_symbol.applicable_kinds = type_parameter -# Instance fields are camelCase and start with _ -dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion -dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style -dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields -dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion -dotnet_naming_rule.instance_fields_should_be_camel_case.style = camel_case_underscore_style -dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +# Naming style shared by the private-field rules: camelCase with a required '_' prefix. dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case dotnet_naming_style.camel_case_underscore_style.required_prefix = _ -dotnet_naming_style.instance_field_style.capitalization = camel_case -dotnet_naming_style.instance_field_style.required_prefix = _ -dotnet_naming_symbols.instance_fields.applicable_kinds = field -dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal -dotnet_naming_symbols.private_internal_fields.applicable_kinds = field # Local functions are PascalCase dotnet_naming_rule.local_functions_should_be_pascal_case.severity = warning -dotnet_naming_rule.local_functions_should_be_pascal_case.style = non_private_static_field_style -dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = all_members +dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions dotnet_naming_style.local_function_style.capitalization = pascal_case dotnet_naming_symbols.local_functions.applicable_kinds = local_function @@ -264,12 +257,10 @@ dotnet_style_qualification_for_property = false:silent dotnet_style_operator_placement_when_wrapping = end_of_line # Naming styles -dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning -dotnet_naming_rule.interface_should_be_begins_with_i.style = prefix_interface_with_i_style -dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface -dotnet_naming_rule.types_should_be_pascal_case.severity = warning -dotnet_naming_rule.types_should_be_pascal_case.style = non_private_static_field_style -dotnet_naming_rule.types_should_be_pascal_case.symbols = types +# NOTE: a 'types_should_be_pascal_case' rule (symbol group 'types') and an +# 'interface_should_be_begins_with_i' rule (symbol group 'interface') used to sit here, but neither +# symbol group was ever defined, so neither rule applied. Types are covered by 'element_rule' and +# interfaces by 'interface_rule' above. # By default, name items with PascalCase dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning @@ -278,7 +269,6 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_m # pascal_case_style - Define the PascalCase style dotnet_naming_style.pascal_case_style.capitalization = pascal_case -dotnet_naming_symbols.all_members.applicable_kinds = * # Symbol specifications dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected @@ -286,7 +276,6 @@ dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, meth dotnet_naming_symbols.non_field_members.required_modifiers = * # Naming styles -dotnet_naming_style._fieldname.capitalization = camel_case dotnet_naming_style.begins_with_i.capitalization = pascal_case dotnet_naming_style.begins_with_i.required_prefix = I dotnet_naming_style.begins_with_i.required_suffix = @@ -314,9 +303,11 @@ dotnet_code_quality.prefer_const = true dotnet_code_quality.prefer_inferred_anonymous_type_member_names = true dotnet_code_quality.prefer_inferred_tuple_names = true dotnet_code_quality.prefer_readonly = true -dotnet_code_quality.require_accessibility_modifiers = true +# The real modifier policy is 'dotnet_style_require_accessibility_modifiers' (declared under +# "Modifier preferences" below). The 'require_accessibility_modifiers' and +# 'require_explicit_visibility' entries that used to live here are not .NET analyzer options: +# declaring them only pretended to enforce a policy that was never applied, so they were removed. dotnet_code_quality.require_explicit_type_arguments = true -dotnet_code_quality.require_explicit_visibility = true dotnet_code_quality.require_variable_declaration_for_explicit_type = false dotnet_code_quality_unused_parameters = all:warning dotnet_enable_roslyn_analyzers = true @@ -587,10 +578,14 @@ dotnet_diagnostic.CA1030.severity = suggestion dotnet_diagnostic.CA1031.severity = error # Implement standard exception constructors -dotnet_diagnostic.CA1032.severity = suggestion +# Raised to warning: the public API surface of exceptions is part of the accessibility policy and +# must not be invisible to command-line builds. +dotnet_diagnostic.CA1032.severity = warning # Interface methods should be callable by child types -dotnet_diagnostic.CA1033.severity = suggestion +# Raised to warning: explicit interface implementations that hide the member from derived types are +# an accessibility problem, not a style preference. +dotnet_diagnostic.CA1033.severity = warning # Nested types should not be visible dotnet_diagnostic.CA1034.severity = error @@ -599,7 +594,9 @@ dotnet_diagnostic.CA1034.severity = error dotnet_diagnostic.CA1036.severity = suggestion # Avoid empty interfaces -dotnet_diagnostic.CA1040.severity = suggestion +# Raised to warning: empty interfaces are unenforceable contracts and were previously invisible to +# command-line builds. +dotnet_diagnostic.CA1040.severity = warning # Provide ObsoleteAttribute message dotnet_diagnostic.CA1041.severity = warning @@ -794,7 +791,9 @@ dotnet_diagnostic.CA1513.severity = warning dotnet_diagnostic.CA1514.severity = warning # Consider making public types internal -dotnet_diagnostic.CA1515.severity = suggestion +# Reported as a real build warning (not a suggestion) so accessibility hygiene cannot be ignored: +# a public type in an application or test assembly should be made internal rather than suppressed. +dotnet_diagnostic.CA1515.severity = warning # Naming Rules (CA1700-CA1727 and IDE0130) # Naming rules support adherence to the naming conventions of the .NET design guidelines @@ -973,8 +972,9 @@ dotnet_diagnostic.CA1845.severity = error # Prefer AsSpan over Substring dotnet_diagnostic.CA1846.severity = error -dotnet_diagnostic.ignore_internalsvisibleto = true -dotnet_diagnostic.CA1852.ignore_internalsvisibleto = true +# CA1852 (seal internal types) is intentionally not suppressed for internal types that are exposed +# to other assemblies through InternalsVisibleTo: those types must still be sealed. The blanket +# suppression that used to sit here relied on a malformed key, so it never even applied. # Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks dotnet_diagnostic.CA2352.severity = error @@ -1319,8 +1319,9 @@ dotnet_diagnostic.IDE0038.severity = warning # Use local function instead of lambda dotnet_diagnostic.IDE0039.severity = suggestion -# Add accessibility modifiers -dotnet_diagnostic.IDE0040.severity = error +# IDE0040 is declared exactly once, in the "Accessibility modifiers" section above. A second entry +# here used to override that value (later entries win), which made the effective modifier policy +# ambiguous and let repos believe the rule had been turned off. # Use is null check dotnet_diagnostic.IDE0041.severity = suggestion @@ -1557,8 +1558,9 @@ dotnet_diagnostic.IDE0380.severity = warning # Remove unnecessary suppression (null-forgiving operator) dotnet_diagnostic.IDE0370.severity = warning -# Naming rule violation -dotnet_diagnostic.IDE1006.severity = silent +# Naming rule violation - must stay visible: the field-naming policy is only enforced when this +# diagnostic is reported, because naming rules without their own severity inherit this one. +dotnet_diagnostic.IDE1006.severity = warning # Embedded statements must be on their own line dotnet_diagnostic.IDE2001.severity = warning @@ -2671,17 +2673,14 @@ dotnet_diagnostic.PDS0004.severity = none [**/Extensions/**.{cs,vb}] # Files under the Extensions/ namespace-reset convention must not force developers to add pragmas; -# suppress the namespace-conflict diagnostics that arise from declaring framework-style namespaces. +# suppress only the namespace-conflict diagnostics that arise from declaring framework-style +# namespaces. Accessibility/design rules (for example CA1034) are deliberately not suppressed here. dotnet_diagnostic.IDE0130.severity = none -dotnet_diagnostic.CA1034.severity = none dotnet_diagnostic.CA1724.severity = none dotnet_diagnostic.CS0436.severity = none dotnet_diagnostic.CS1591.severity = none dotnet_diagnostic.IDE0005.severity = none -[**/{Extension,Extensions}.{cs,vb}] -dotnet_diagnostic.CA1034.severity = none - [**/Generated/**/*.{cs,vb}] generated_code = true dotnet_diagnostic.CS8602.severity = none diff --git a/README.md b/README.md index 6e5a05d..459ebfb 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A reusable MSBuild SDK NuGet package that delivers standardised .NET project def | -- | -- | | **Project type detection** | `IsCSharpProject`, `IsTestProject`, `IsSharedTestingProject`, `IsContainerProject`, `IsWebSdkProject`, `IsAspireHostProject`, … | | **C# defaults** | `net10.0` TFM (overridable), `LangVersion=preview`, `Nullable=enable`, `ImplicitUsings=enable`, deterministic builds | -| **Code style** | `.editorconfig` baked into the package, applied via `EditorConfigFilePath`, and auto-bootstrapped to repo root if missing; `EnforceCodeStyleInBuild=true`, `EnableNETAnalyzers=true`, `AnalysisLevel=latest`, `AnalysisMode=All` | +| **Code style** | `.editorconfig` baked into the package, applied via `EditorConfigFilePath`, and auto-bootstrapped to repo root if missing; `EnforceCodeStyleInBuild=true`, `EnableNETAnalyzers=true`, `AnalysisLevel=latest`, `AnalysisMode=All`. Style policy: `dotnet_style_require_accessibility_modifiers=omit_if_default`, private instance fields named `_camelCase`, and `IDE0040`/`CA1515`/`CA1852`/`CA1034` at warning or above, enforced by `ValidatePurviewStylePolicy` (opt out with `DisablePurviewStylePolicyValidation=true`) | | **NuGet packaging** | `AssemblyName`/`PackageId` default to the fully evaluated `RootNamespace`; packable projects get `GenerateDocumentationFile=true`, `PublishRepositoryUrl=true`, `IncludeSymbols=true`, `SymbolPackageFormat=snupkg`, `EmbedUntrackedSources=true`, and portable PDBs delivered via `.snupkg` (not the `.nupkg`) | | **Repo bootstrap** | Missing repo-root `.editorconfig` and `global.json` are auto-copied/created by default (disable via `DisableAutoCopySdkFiles=true`) | | **CI detection** | `ContinuousIntegrationBuild` set automatically when `CI`, `GITHUB_ACTIONS`, or `TF_BUILD` env vars are present | @@ -237,6 +237,7 @@ Version detection logging is disabled by default. Set `VersionDetectionLogEnable | -- | -- | -- | | `NamespacePrefix` | *(required)* | Root namespace prefix, e.g. `Acme`. Results in `Acme.MyProject`. | | `DisableNamespacePrefixCheck` | `false` | Set to `true` to suppress the build error for missing `NamespacePrefix`. | +| `DisablePurviewStylePolicyValidation` | `false` | Set to `true` to stop the build failing (`PRSGD0006`-`PRSGD0009`) when the repository `.editorconfig` overrides the modifier policy, hides `IDE0040`/`IDE1006`, disables the Style category in bulk, weakens the `_camelCase` field-naming rule, or adds the accessibility rules to `NoWarn`. | | `TargetFramework` | `net10.0` | Override the default TFM per-project or globally. Defaults to `netstandard2.0` for projects declaring `IsRoslynComponent=true`. | | `IsRoslynComponent` | `false` | When explicitly `true`, applies source-generator defaults: a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `Deterministic=true`, extended analyzer rules, SourceLink with `EmbedUntrackedSources=true`, compiler-generated output under the intermediate directory, no dependency file, telemetry exclusion, and package build output. Pack-time validation (`ValidateRoslynComponentCompilerSettings`) fails the pack if the compiler defaults are missing unless `DisableRoslynCompilerDefaultsValidation=true`. Roslyn development dependencies (`Microsoft.CodeAnalysis.*`, `Microsoft.CodeAnalysis.Analyzers`) default to `PrivateAssets="all"`. | | `IsRoslynComponentOnly` | `true` for Roslyn components | Produces an analyzer-only package: `IncludeBuildOutput=false`, `IncludeSymbols=false`, and the analyzer assembly and portable PDB are packed under `analyzers/dotnet/cs/`. Set to `false` for a dual-role Roslyn component that uses normal library symbol packaging. | @@ -532,6 +533,39 @@ The package ships `Purview.BuildSdk.Analyzers.dll` (plus a separate code-fix ass and adds the analyzer to every C# project as an `` item, so the rules surface in both command-line builds and Visual Studio. +### Style policy + +The shipped `.editorconfig` enforces a modifier/visibility **and field-naming** policy, and it is +deliberately not silenceable: + +- `dotnet_style_require_accessibility_modifiers = omit_if_default:warning` — a declared modifier that + matches the language default (`private` inside a type, `internal` at namespace scope, `public` inside + an interface) is reported by `IDE0040` and must be **removed**. Omit the default modifier instead. +- Private instance fields must be `_camelCase`, never `camelCase` or `PascalCase`: the `_` prefix keeps + field access unambiguous, so the noisy `this.` qualifier is never needed. Constants and `static + readonly` fields stay PascalCase, and local constants stay camelCase. +- `CA1515` (public type in an application/test assembly) ships at **warning**, `CA1852` (seal internal + types) is no longer suppressed for internals exposed through `InternalsVisibleTo`, and `CA1034` is no + longer disabled for `Extensions/` files. +- Test projects are executables, so test classes must be non-public (`sealed class MyTests`, which is + `internal` by default) instead of `CA1515`/`CA1707`/`CA1062` being hidden through `NoWarn`. + +`ValidatePurviewStylePolicy` runs before every C# compile and fails the build when a repository neuters +the policy: + +| Code | Fails when | +| -- | -- | +| `PRSGD0006` | `dotnet_style_require_accessibility_modifiers` is overridden with anything other than `omit_if_default`. | +| `PRSGD0007` | A policy rule (`IDE0040`, `CA1515`, `CA1852`, `CA1034`) is downgraded below warning, any policy rule is set to `none`/`silent`, or the `Style` category is disabled in bulk without an explicit `IDE0040` severity. | +| `PRSGD0008` | `IDE0040`, `CA1515`, `CA1852`, `CA1034`, `CA1012`, `CA1047`, `CA1050`, `CA1051`, `CA1062`, `CA1064`, or `CA1707` is added to `NoWarn`. | +| `PRSGD0009` | `IDE1006` is hidden, the private instance field rule is downgraded below warning, or the `_` prefix is removed from the field naming style. | + +The validator walks the same `.editorconfig` chain as the compiler (nearest file wins, later entries +beat earlier ones, `root = true` stops the walk). Because a repository can keep an older bootstrapped +copy of the shipped file, a stale copy is reported rather than obeyed: refresh it with +`PurviewRepoBootstrapMode=Always` (or delete it), or opt out per repository/project with +`DisablePurviewStylePolicyValidation=true`. + | Rule | Category | Severity | Description | | -- | -- | -- | -- | | `PDS0001` | (suppressor) | — | Suppresses `CS1591` for `EditorBrowsable(Never)` members | diff --git a/docs/wiki/Analyzers.md b/docs/wiki/Analyzers.md index 0ca5c3c..ec43bc9 100644 --- a/docs/wiki/Analyzers.md +++ b/docs/wiki/Analyzers.md @@ -12,6 +12,34 @@ command-line builds and Visual Studio. | `PDS0004` | Naming | Warning | Use correct acronym capitalization (`Api` → `API`) | | `PDS0005` | (suppressor) | — | Suppresses `IDE0130` for files rooted under `Extensions/` | +## Style policy + +The SDK ships the modifier/visibility **and field-naming** policy in `.editorconfig` and enforces it with +`ValidatePurviewStylePolicy` (`PRSGD0006`-`PRSGD0009`), so a repository cannot neuter it: + +- `dotnet_style_require_accessibility_modifiers = omit_if_default:warning`: a declared modifier that + matches the language default (`private` inside a type, `internal` at namespace scope, `public` inside + an interface) is reported by `IDE0040` and must be removed. +- Private instance fields must be `_camelCase` (`_name`, never `name`): the `_` prefix keeps field + access unambiguous, so the noisy `this.` qualifier is never needed. Constants and `static readonly` + fields stay PascalCase, and local constants stay camelCase. +- `CA1515` (public type in an application/test assembly) ships at warning; `CA1852` (seal internal + types) is not suppressed for types exposed through `InternalsVisibleTo`; `CA1034` is not disabled for + `Extensions/` files; the API-surface rules `CA1062`/`CA1707` are not pre-suppressed for test projects. +- Test projects are executables, so their test classes must be non-public (`sealed class MyTests`, + `internal` by default) rather than the rules being hidden through `NoWarn`. + +| Code | Reported when | +| -- | -- | +| `PRSGD0006` | `dotnet_style_require_accessibility_modifiers` is set to anything other than `omit_if_default`. | +| `PRSGD0007` | A policy rule is downgraded below warning (`IDE0040`, `CA1515`, `CA1852`, `CA1034`), any policy rule is set to `none`/`silent`, or the `Style` category is disabled in bulk without an explicit `IDE0040` severity. | +| `PRSGD0008` | One of the accessibility rules (`IDE0040`, `CA1515`, `CA1852`, `CA1034`, `CA1012`, `CA1047`, `CA1050`, `CA1051`, `CA1062`, `CA1064`, `CA1707`) is added to `NoWarn`. | +| `PRSGD0009` | `IDE1006` is hidden, the private instance field naming rule is downgraded below warning, or the `_` prefix is removed from the field naming style. | + +Opt out with `true`. A stale +bootstrapped `.editorconfig` is reported rather than obeyed — refresh it with +`PurviewRepoBootstrapMode=Always` (or delete it). + ## PDS0002 — Extensions namespace rule When a file is placed under a project-root `Extensions/` folder, the analyzer intentionally treats diff --git a/docs/wiki/Configuration-Reference.md b/docs/wiki/Configuration-Reference.md index cb6f1ac..335b570 100644 --- a/docs/wiki/Configuration-Reference.md +++ b/docs/wiki/Configuration-Reference.md @@ -20,6 +20,7 @@ See [Version Detection](Version-Detection.md) for the full resolution rules. | -- | -- | -- | | `NamespacePrefix` | *(required)* | Root namespace prefix, e.g. `Acme`. Results in `Acme.MyProject`. | | `DisableNamespacePrefixCheck` | `false` | Set to `true` to suppress the build error for missing `NamespacePrefix`. | +| `DisablePurviewStylePolicyValidation` | `false` | Set to `true` to stop `ValidatePurviewStylePolicy` failing the build (`PRSGD0006`-`PRSGD0009`) when the repository `.editorconfig` overrides `dotnet_style_require_accessibility_modifiers`, hides `IDE0040`/`IDE1006`, disables the Style category in bulk, weakens the `_camelCase` private instance field rule, or adds the accessibility rules to `NoWarn`. See [Style policy](Analyzers.md#style-policy). | | `TargetFramework` | `net10.0` | Override the default TFM per-project or globally. Defaults to `netstandard2.0` for projects declaring `IsRoslynComponent=true`. | | `IsRoslynComponent` | `false` | When explicitly `true`, applies source-generator defaults: a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `Deterministic=true`, extended analyzer rules, SourceLink with `EmbedUntrackedSources=true`, compiler-generated output under the intermediate directory, no dependency file, symbol packaging (`IncludeSymbols=false` by default), telemetry exclusion, and package build output. Packable Roslyn components automatically pack the built analyzer assembly and its PDB into `analyzers/dotnet/cs/` (`PurviewPackAnalyzerPdb=true`; set `false` only when symbols are delivered another way — NuGet's `.snupkg` cannot host `analyzers/dotnet/cs` symbols). Pack-time validation (`ValidateRoslynComponentCompilerSettings`) fails the pack if the compiler defaults are missing unless `DisableRoslynCompilerDefaultsValidation=true`. Roslyn development dependencies (`Microsoft.CodeAnalysis.*`, `Microsoft.CodeAnalysis.Analyzers`) default to `PrivateAssets="all"`. | | `PackProjectReferencedSourceGenerators` | `true` | Automatically packs analyzer `ProjectReference` outputs and their runtime dependencies under `analyzers/dotnet/cs/`. Set to `false` to opt out; set `Pack="false"` on an individual reference to exclude only that generator. | diff --git a/docs/wiki/Repository-Bootstrap.md b/docs/wiki/Repository-Bootstrap.md index 88a9d1e..e6c6599 100644 --- a/docs/wiki/Repository-Bootstrap.md +++ b/docs/wiki/Repository-Bootstrap.md @@ -13,6 +13,16 @@ The package ships an `.editorconfig` in `Sdk/.editorconfig`. It is: 2. Written to the repository root (as a physical file) when a `.editorconfig` does not already exist there, so tools like CSharpier pick it up. +The shipped file also defines the style policy +(`dotnet_style_require_accessibility_modifiers = omit_if_default`, private instance fields named +`_camelCase`, with `IDE0040`, `CA1515`, `CA1852` and `CA1034` at warning or above). Because a repository +owner can edit a bootstrapped copy, the `ValidatePurviewStylePolicy` target re-checks the *effective* +configuration on every compile and fails the build (`PRSGD0006`-`PRSGD0009`) when that policy has been +overridden, hidden, or silenced through `NoWarn`. A stale copy that predates the current policy (for +example one that still hides `IDE1006`) is therefore reported rather than obeyed — refresh it with +`PurviewRepoBootstrapMode=Always`. Opt out with `DisablePurviewStylePolicyValidation=true`; see +[Style policy](Analyzers.md#style-policy). + The write is atomic (a temporary file is renamed into place) and retried quietly, so parallel projects sharing the repository root cannot corrupt it or fail the build by racing. An existing file is never overwritten unless you ask for it. diff --git a/package.json b/package.json index b332504..2eba7cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "purview-build-sdk", - "version": "1.0.0", + "version": "1.0.1.0", "homepage": "https://purview.dev/projects/build-sdk/", "bugs": { "url": "https://github.com/purview-dev/build-sdk/issues" diff --git a/src/src/BuildSdk/Sdk/.agents/skills/sdk-configuration-reference/SKILL.md b/src/src/BuildSdk/Sdk/.agents/skills/sdk-configuration-reference/SKILL.md index b516205..d9ef035 100644 --- a/src/src/BuildSdk/Sdk/.agents/skills/sdk-configuration-reference/SKILL.md +++ b/src/src/BuildSdk/Sdk/.agents/skills/sdk-configuration-reference/SKILL.md @@ -43,6 +43,7 @@ These are the most important configurable properties exposed by the SDK: - `NamespacePrefix` — required unless `DisableNamespacePrefixCheck=true` - `DisableNamespacePrefixCheck` — default `false` +- `DisablePurviewStylePolicyValidation` — default `false`; set to `true` to stop the build failing (`PRSGD0006`-`PRSGD0009`) when the repository `.editorconfig` overrides the modifier policy (`dotnet_style_require_accessibility_modifiers` other than `omit_if_default`), hides `IDE0040`/`IDE1006`, disables the Style category in bulk, weakens the `_camelCase` private instance field naming rule, or adds the accessibility rules to `NoWarn` - `TargetFramework` — defaults to `net10.0` when neither `TargetFramework` nor `TargetFrameworks` is set; projects explicitly declaring `IsRoslynComponent=true` default to `netstandard2.0` - `IsRoslynComponent` — when explicitly `true`, applies source-generator defaults: a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `Deterministic=true`, extended analyzer rules, SourceLink with `EmbedUntrackedSources=true`, no dependency file, compiler-generated output under the framework-specific intermediate directory, telemetry exclusion, and `PrivateAssets=all` applied to `Microsoft.CodeAnalysis.*` / `Microsoft.CodeAnalysis.Analyzers` references. Packable Roslyn components automatically pack the built analyzer assembly and PDB into `analyzers/dotnet/cs/`; a pack-time validation (`ValidateRoslynComponentCompilerSettings`) fails the pack if the compiler defaults are missing unless `DisableRoslynCompilerDefaultsValidation=true` - `IsRoslynComponentOnly` — defaults to `true` for Roslyn components and creates an analyzer-only package: it sets `IncludeBuildOutput=false`, `IncludeSymbols=false`, and packages the portable PDB alongside the analyzer under `analyzers/dotnet/cs/`. Set it to `false` for a dual-role Roslyn component that uses normal library symbol packaging. diff --git a/src/src/BuildSdk/Sdk/Props/Defaults.props b/src/src/BuildSdk/Sdk/Props/Defaults.props index d439aa8..18f782f 100644 --- a/src/src/BuildSdk/Sdk/Props/Defaults.props +++ b/src/src/BuildSdk/Sdk/Props/Defaults.props @@ -33,6 +33,8 @@ >false false + false false false diff --git a/src/src/BuildSdk/Sdk/Sdk.props b/src/src/BuildSdk/Sdk/Sdk.props index e46a8a2..866d6cb 100644 --- a/src/src/BuildSdk/Sdk/Sdk.props +++ b/src/src/BuildSdk/Sdk/Sdk.props @@ -487,12 +487,6 @@ Condition="'$(IsAspireHostProject)' == 'true' AND '$(IsTestProject)' != 'true' AND '$(IsSharedTestingProject)' != 'true'" > Exe - - $(NoWarn);CA1515; Exe - - $(NoWarn);CA1515; @@ -839,7 +827,7 @@ Condition="'$(IsTestProject)' == 'true' OR '$(IsSharedTestingProject)' == 'true'" Label="C# Shared Testing Project Properties" > - $(NoWarn);CA1062;CA1515;CA1707;CA1822; + $(NoWarn);CA1822; $(NoWarn);CS1591; false false diff --git a/src/src/BuildSdk/Sdk/Sdk.targets b/src/src/BuildSdk/Sdk/Sdk.targets index 2848c10..4493e7b 100644 --- a/src/src/BuildSdk/Sdk/Sdk.targets +++ b/src/src/BuildSdk/Sdk/Sdk.targets @@ -1496,6 +1496,282 @@ /> + + + + + + + + + + + + + + + + (StringComparer.OrdinalIgnoreCase) + { + "IDE0040", "CA1515", "CA1852", "CA1034" + }; + var hiddenSeverities = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "none", "silent", "suggestion" + }; + var offSeverities = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "none", "silent" + }; + + if (!string.IsNullOrEmpty(NoWarn)) + { + foreach (var raw in NoWarn.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries)) + { + var id = raw.Trim(); + if (id.Length == 0) + continue; + + foreach (var rule in policyRules) + { + if (!string.Equals(id, rule, StringComparison.OrdinalIgnoreCase)) + continue; + + Log.LogError(null, "PRSGD0008", null, null, 0, 0, 0, 0, + "The project silences the accessibility rule '{0}' through NoWarn. Accessibility modifier and visibility rules are part of the Purview.BuildSdk policy and must be fixed rather than suppressed; remove '{0}' from NoWarn, or set DisablePurviewStylePolicyValidation=true to opt out of this validation.", id); + } + } + } + + // Walk the .editorconfig chain the way the compiler does: the nearest file wins, + // later entries beat earlier ones, and the walk stops at a file declaring 'root = true'. + var chain = new List(); + var directory = ProjectDirectory; + while (!string.IsNullOrEmpty(directory) && Directory.Exists(directory)) + { + var candidate = Path.Combine(directory, ".editorconfig"); + if (File.Exists(candidate)) + { + chain.Add(candidate); + if (DeclaresRoot(candidate)) + break; + } + + var trimmed = directory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var parent = Path.GetDirectoryName(trimmed); + if (string.IsNullOrEmpty(parent) || string.Equals(parent, directory, StringComparison.OrdinalIgnoreCase)) + break; + + directory = parent; + } + + chain.Reverse(); + + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); + var sources = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var file in chain) + MergeEditorConfig(file, values, sources); + + string optionValue; + if (values.TryGetValue("dotnet_style_require_accessibility_modifiers", out optionValue)) + { + if (!string.Equals(optionValue.Split(':')[0].Trim(), "omit_if_default", StringComparison.OrdinalIgnoreCase)) + { + Log.LogError(null, "PRSGD0006", null, sources["dotnet_style_require_accessibility_modifiers"], 0, 0, 0, 0, + "This repository overrides 'dotnet_style_require_accessibility_modifiers' with '{0}'. The Purview.BuildSdk modifier policy requires 'omit_if_default': a declared accessibility modifier that matches the language default is noise and must be removed. Remove the override, or set DisablePurviewStylePolicyValidation=true to opt out of this validation.", optionValue); + } + } + + string ide0040; + if (values.TryGetValue("dotnet_diagnostic.IDE0040.severity", out ide0040)) + { + if (hiddenSeverities.Contains(ide0040.Split(':')[0].Trim())) + { + Log.LogError(null, "PRSGD0007", null, sources["dotnet_diagnostic.IDE0040.severity"], 0, 0, 0, 0, + "This repository sets 'dotnet_diagnostic.IDE0040.severity' to '{0}'. The accessibility modifier policy cannot be turned off or hidden; remove the override, or set DisablePurviewStylePolicyValidation=true to opt out of this validation.", ide0040); + } + } + else if (IsHidden(values, "dotnet_analyzer_diagnostic.category-Style.severity") || IsHidden(values, "dotnet_analyzer_diagnostic.severity")) + { + Log.LogError(null, "PRSGD0007", null, chain.Count > 0 ? chain[chain.Count - 1] : null, 0, 0, 0, 0, + "This repository disables analyzer rules in bulk, which hides IDE0040 ('dotnet_analyzer_diagnostic.*'). Configure the Style category (or all analyzers) to a visible severity, or set DisablePurviewStylePolicyValidation=true to opt out of this validation."); + } + + foreach (var rule in policyRules) + { + var key = "dotnet_diagnostic." + rule + ".severity"; + string ruleSeverity; + if (!values.TryGetValue(key, out ruleSeverity)) + continue; + + var forbidden = mustStayAtWarning.Contains(rule) ? hiddenSeverities : offSeverities; + if (!forbidden.Contains(ruleSeverity.Split(':')[0].Trim())) + continue; + + Log.LogError(null, "PRSGD0007", null, sources[key], 0, 0, 0, 0, + "This repository sets '{0}' to '{1}'. The accessibility policy requires '{0}' to stay at warning or above; remove the override, or set DisablePurviewStylePolicyValidation=true to opt out of this validation.", key, ruleSeverity); + } + + // Field-naming policy: private instance fields must be '_camelCase', and the naming + // diagnostic that reports the violation must stay visible. + var namingSeverityKey = "dotnet_diagnostic.IDE1006.severity"; + string namingSeverity; + if (values.TryGetValue(namingSeverityKey, out namingSeverity)) + { + if (hiddenSeverities.Contains(namingSeverity.Split(':')[0].Trim())) + { + Log.LogError(null, "PRSGD0009", null, sources[namingSeverityKey], 0, 0, 0, 0, + "This repository sets '{0}' to '{1}', which hides every naming rule - including the '_'-prefixed private instance field rule. Keep it at warning or above (a stale bootstrapped .editorconfig can be refreshed with PurviewRepoBootstrapMode=Always), or set DisablePurviewStylePolicyValidation=true to opt out of this validation.", namingSeverityKey, namingSeverity); + } + } + + var fieldNamingRuleKey = "dotnet_naming_rule.private_instance_fields_must_be_camel_case_with_underscore_prefix.severity"; + string fieldNamingSeverity; + if (values.TryGetValue(fieldNamingRuleKey, out fieldNamingSeverity)) + { + if (hiddenSeverities.Contains(fieldNamingSeverity.Split(':')[0].Trim())) + { + Log.LogError(null, "PRSGD0009", null, sources[fieldNamingRuleKey], 0, 0, 0, 0, + "This repository sets '{0}' to '{1}'. Private instance fields must be named '_camelCase' so field access never needs the noisy 'this.' qualifier; keep the rule at warning or above, or set DisablePurviewStylePolicyValidation=true to opt out of this validation.", fieldNamingRuleKey, fieldNamingSeverity); + } + } + + var fieldNamingPrefixKey = "dotnet_naming_style.camel_case_underscore_style.required_prefix"; + string fieldNamingPrefix; + if ( + values.TryGetValue(fieldNamingPrefixKey, out fieldNamingPrefix) + && !string.Equals(fieldNamingPrefix.Trim(), "_", StringComparison.Ordinal) + ) + { + Log.LogError(null, "PRSGD0009", null, sources[fieldNamingPrefixKey], 0, 0, 0, 0, + "This repository sets '{0}' to '{1}'. Private instance fields must keep the '_' prefix ('_name', never 'name'); restore the prefix, or set DisablePurviewStylePolicyValidation=true to opt out of this validation.", fieldNamingPrefixKey, fieldNamingPrefix); + } + + bool DeclaresRoot(string file) + { + string[] lines; + try + { + lines = File.ReadAllLines(file); + } + catch (Exception) + { + return false; + } + + foreach (var line in lines) + { + var text = StripComment(line).Trim(); + if (text.Length == 0 || text[0] == '[') + continue; + + var separator = text.IndexOf('='); + if (separator <= 0) + continue; + + if (!string.Equals(text.Substring(0, separator).Trim(), "root", StringComparison.OrdinalIgnoreCase)) + continue; + + if (string.Equals(text.Substring(separator + 1).Trim(), "true", StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + + void MergeEditorConfig(string file, Dictionary target, Dictionary targetSources) + { + string[] lines; + try + { + lines = File.ReadAllLines(file); + } + catch (Exception) + { + return; + } + + var section = string.Empty; + foreach (var line in lines) + { + var text = StripComment(line).Trim(); + if (text.Length == 0) + continue; + + if (text[0] == '[') + { + var end = text.IndexOf(']'); + section = end > 0 ? text.Substring(0, end + 1) : text; + continue; + } + + var separator = text.IndexOf('='); + if (separator <= 0 || !SectionAppliesToCSharp(section)) + continue; + + var key = text.Substring(0, separator).Trim(); + target[key] = text.Substring(separator + 1).Trim(); + targetSources[key] = file; + } + } + + string StripComment(string line) + { + var index = line.IndexOf('#'); + return index < 0 ? line : line.Substring(0, index); + } + + bool SectionAppliesToCSharp(string section) + { + if (section.Length == 0 || string.Equals(section, "[*]", StringComparison.OrdinalIgnoreCase)) + return true; + + return section.IndexOf(".cs", StringComparison.OrdinalIgnoreCase) >= 0; + } + + bool IsHidden(Dictionary source, string key) + { + string value; + if (!source.TryGetValue(key, out value)) + return false; + + return hiddenSeverities.Contains(value.Split(':')[0].Trim()); + } + ]]> + + + + + + + + . /// [Category("Integration")] -public sealed class EnglishNamingTests +sealed class EnglishNamingTests { [Test] public async Task Analyzer_TypeNamedApi_ReportsDiagnostic(CancellationToken cancellationToken) diff --git a/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceAnalyzerTests.cs b/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceAnalyzerTests.cs index ed79907..bfcd7a4 100644 --- a/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceAnalyzerTests.cs +++ b/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceAnalyzerTests.cs @@ -11,7 +11,7 @@ namespace Purview.BuildSdk.Analyzers.Extensions; /// testing harness with TUnit. /// [Category("Integration")] -public sealed class ExtensionsNamespaceAnalyzerTests +sealed class ExtensionsNamespaceAnalyzerTests { static async Task> AnalyzeAsync( string filePath, diff --git a/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceCodeFixTests.cs b/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceCodeFixTests.cs index 42752fb..e4d4a74 100644 --- a/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceCodeFixTests.cs +++ b/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceCodeFixTests.cs @@ -15,7 +15,7 @@ namespace Purview.BuildSdk.Analyzers.IntegrationTests.Extensions; /// Integration tests for . /// [Category("Integration")] -public sealed class ExtensionsNamespaceCodeFixTests +sealed class ExtensionsNamespaceCodeFixTests { static async Task ApplyCodeFixAsync(string fileName, string source, CancellationToken cancellationToken) { diff --git a/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceSuppressorTests.cs b/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceSuppressorTests.cs index c74beb3..282bad9 100644 --- a/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceSuppressorTests.cs +++ b/src/tests/Analyzers.IntegrationTests/Extensions/ExtensionsNamespaceSuppressorTests.cs @@ -9,7 +9,7 @@ namespace Purview.BuildSdk.Analyzers.IntegrationTests.Extensions; /// Integration tests for behavior. /// [Category("Integration")] -public sealed class ExtensionsNamespaceSuppressorTests +sealed class ExtensionsNamespaceSuppressorTests { static async Task> AnalyzeAsync( string filePath, diff --git a/src/tests/Analyzers.IntegrationTests/Extensions/SplitExtensionsClassTests.cs b/src/tests/Analyzers.IntegrationTests/Extensions/SplitExtensionsClassTests.cs index 4ffb143..58c2a58 100644 --- a/src/tests/Analyzers.IntegrationTests/Extensions/SplitExtensionsClassTests.cs +++ b/src/tests/Analyzers.IntegrationTests/Extensions/SplitExtensionsClassTests.cs @@ -14,7 +14,7 @@ namespace Purview.BuildSdk.Analyzers.IntegrationTests.Extensions; /// Integration tests for . /// [Category("Integration")] -public sealed class SplitExtensionsClassTests +sealed class SplitExtensionsClassTests { const string SplitReceiverSource = """ namespace Microsoft.Extensions.Hosting diff --git a/src/tests/Analyzers.IntegrationTests/TargetTypedObjectCreation/TargetTypedObjectCreationTests.cs b/src/tests/Analyzers.IntegrationTests/TargetTypedObjectCreation/TargetTypedObjectCreationTests.cs index 215215b..3b5498b 100644 --- a/src/tests/Analyzers.IntegrationTests/TargetTypedObjectCreation/TargetTypedObjectCreationTests.cs +++ b/src/tests/Analyzers.IntegrationTests/TargetTypedObjectCreation/TargetTypedObjectCreationTests.cs @@ -9,7 +9,7 @@ namespace Purview.BuildSdk.Analyzers.TargetTypedObjectCreation; -public sealed class TargetTypedObjectCreationTests +sealed class TargetTypedObjectCreationTests { [Test] public async Task Analyzer_MethodCallResult_DoesNotReportDiagnostic(CancellationToken cancellationToken) diff --git a/src/tests/Analyzers.UnitTests/EditorBrowsable/EditorBrowsableSuppressorTests.cs b/src/tests/Analyzers.UnitTests/EditorBrowsable/EditorBrowsableSuppressorTests.cs index 1705113..82a4392 100644 --- a/src/tests/Analyzers.UnitTests/EditorBrowsable/EditorBrowsableSuppressorTests.cs +++ b/src/tests/Analyzers.UnitTests/EditorBrowsable/EditorBrowsableSuppressorTests.cs @@ -11,7 +11,7 @@ namespace Purview.BuildSdk.Analyzers.EditorBrowsable; /// InternalsVisibleTo attribute that Sdk.targets auto-generates for every /// <TestType> variant when building the analyzer project. /// -public sealed class EditorBrowsableSuppressorTests +sealed class EditorBrowsableSuppressorTests { static readonly string[] TrustedAssemblies = ( (string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") ?? "" diff --git a/src/tests/Analyzers.UnitTests/EnglishNaming/EnglishNamingHelperTests.cs b/src/tests/Analyzers.UnitTests/EnglishNaming/EnglishNamingHelperTests.cs index 1c16edb..9cc3bbd 100644 --- a/src/tests/Analyzers.UnitTests/EnglishNaming/EnglishNamingHelperTests.cs +++ b/src/tests/Analyzers.UnitTests/EnglishNaming/EnglishNamingHelperTests.cs @@ -8,7 +8,7 @@ namespace Purview.BuildSdk.Analyzers.EnglishNaming; /// .editorconfig-driven customisation. /// [Category("Unit")] -public sealed class EnglishNamingHelperTests +sealed class EnglishNamingHelperTests { static readonly EnglishNamingConfig Defaults = EnglishNamingConfig.Create( EnglishNamingHelper.DefaultAcronymMap, diff --git a/src/tests/Analyzers.UnitTests/ExtensionsNamespace/NamespaceCalculatorTests.cs b/src/tests/Analyzers.UnitTests/ExtensionsNamespace/NamespaceCalculatorTests.cs index 3da3a6c..53f2207 100644 --- a/src/tests/Analyzers.UnitTests/ExtensionsNamespace/NamespaceCalculatorTests.cs +++ b/src/tests/Analyzers.UnitTests/ExtensionsNamespace/NamespaceCalculatorTests.cs @@ -4,7 +4,7 @@ namespace Purview.BuildSdk.Analyzers.ExtensionsNamespace; /// Unit tests for namespace derivation logic. /// [Category("Unit")] -public sealed class NamespaceCalculatorTests +sealed class NamespaceCalculatorTests { [Test] [Arguments( diff --git a/src/tests/BuildSdk.IntegrationTests/AgentPackFolderTests.cs b/src/tests/BuildSdk.IntegrationTests/AgentPackFolderTests.cs index 9112d5e..5e77bcd 100644 --- a/src/tests/BuildSdk.IntegrationTests/AgentPackFolderTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/AgentPackFolderTests.cs @@ -8,7 +8,7 @@ namespace Purview.BuildSdk; /// /// Verifies the Sdk/.agents folder packaging workflow and PurviewAutoSdkPack behaviour. /// -public sealed class AgentPackFolderTests +sealed class AgentPackFolderTests { [Test] public async Task PurviewAutoSdkPack_PacksPhysicalAndLinkedRootAssets(CancellationToken cancellationToken) diff --git a/src/tests/BuildSdk.IntegrationTests/AutoSharedProjectReferencingTests.cs b/src/tests/BuildSdk.IntegrationTests/AutoSharedProjectReferencingTests.cs index 3065d22..5ad3caf 100644 --- a/src/tests/BuildSdk.IntegrationTests/AutoSharedProjectReferencingTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/AutoSharedProjectReferencingTests.cs @@ -9,7 +9,7 @@ namespace Purview.BuildSdk; /// feature that allows projects to transparently depend on shared libraries without /// explicit ProjectReference declarations. /// -public sealed class AutoSharedProjectReferencingTests +sealed class AutoSharedProjectReferencingTests { [Test] public async Task AspireHostProject_WithSharedProject_HasIsAspireProjectResourceFalse( diff --git a/src/tests/BuildSdk.IntegrationTests/CompilerVisiblePropertyTests.cs b/src/tests/BuildSdk.IntegrationTests/CompilerVisiblePropertyTests.cs index 8242798..cce0037 100644 --- a/src/tests/BuildSdk.IntegrationTests/CompilerVisiblePropertyTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/CompilerVisiblePropertyTests.cs @@ -6,7 +6,7 @@ namespace Purview.BuildSdk; /// Verifies SDK properties are exported as CompilerVisibleProperty items so Roslyn /// analyzers/source generators can consume them via build_property.*. /// -public sealed class CompilerVisiblePropertyTests +sealed class CompilerVisiblePropertyTests { [Test] public async Task CompilerVisibleProperties_Include_AllSdkProperties(CancellationToken cancellationToken) diff --git a/src/tests/BuildSdk.IntegrationTests/CoreDefaultsTests.cs b/src/tests/BuildSdk.IntegrationTests/CoreDefaultsTests.cs index 2df1f11..29b258f 100644 --- a/src/tests/BuildSdk.IntegrationTests/CoreDefaultsTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/CoreDefaultsTests.cs @@ -8,7 +8,7 @@ namespace Purview.BuildSdk; /// Default-property assertions are batched into a single MSBuild evaluation per test /// so the suite stays fast without losing any assertion. /// -public sealed class CoreDefaultsTests +sealed class CoreDefaultsTests { [Test] public async Task DefaultProject_CSharpCompilerDefaults(CancellationToken cancellationToken) diff --git a/src/tests/BuildSdk.IntegrationTests/DefaultsPropsTests.cs b/src/tests/BuildSdk.IntegrationTests/DefaultsPropsTests.cs index a392c3a..5c199ae 100644 --- a/src/tests/BuildSdk.IntegrationTests/DefaultsPropsTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/DefaultsPropsTests.cs @@ -2,7 +2,7 @@ namespace Purview.BuildSdk; -public sealed class DefaultsPropsTests +sealed class DefaultsPropsTests { [Test] public async Task NonCsprojEvaluation_DoesNotFailOnBooleanConditions(CancellationToken cancellationToken) diff --git a/src/tests/BuildSdk.IntegrationTests/FieldNamingPolicyTests.cs b/src/tests/BuildSdk.IntegrationTests/FieldNamingPolicyTests.cs new file mode 100644 index 0000000..d037316 --- /dev/null +++ b/src/tests/BuildSdk.IntegrationTests/FieldNamingPolicyTests.cs @@ -0,0 +1,86 @@ +using Purview.BuildSdk.Harness; +using Purview.BuildSdk.Infra; + +namespace Purview.BuildSdk; + +/// +/// Verifies the shipped field-naming policy: private instance fields are named '_camelCase', never +/// 'camelCase', so field access never needs the noisy 'this.' qualifier and fields stay +/// distinguishable from locals and parameters. +/// +sealed class FieldNamingPolicyTests +{ + // The harness only declares SourceLink centrally, so the automatically injected telemetry package + // references are removed like the other harness-based tests do. + const string RemoveTelemetryItems = """ + + + """; + + [Test] + public async Task PrivateInstanceField_WithoutUnderscorePrefix_IsReported(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync( + "MyApp", + extraItems: RemoveTelemetryItems, + cancellationToken: cancellationToken + ); + await ApplyShippedStylePolicyAsync(h, cancellationToken); + await WriteProbeAsync(h, "counter", cancellationToken); + + var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken); + + await Assert.That(success).IsTrue().Because(TestHelpers.GenerateError(output, errors)); + await Assert.That(output + errors).Contains("IDE1006"); + await Assert.That(output + errors).Contains("Missing prefix: '_'"); + } + + [Test] + public async Task PrivateInstanceField_WithUnderscorePrefix_IsNotReported(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync( + "MyApp", + extraItems: RemoveTelemetryItems, + cancellationToken: cancellationToken + ); + await ApplyShippedStylePolicyAsync(h, cancellationToken); + await WriteProbeAsync(h, "_counter", cancellationToken); + + var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken); + + await Assert.That(success).IsTrue().Because(TestHelpers.GenerateError(output, errors)); + await Assert.That(output + errors).DoesNotContain("IDE1006"); + } + + /// + /// The SDK bootstraps a physical .editorconfig during the first build, which is later than the + /// compiler's configuration discovery for that same build. A real repository has the file in place + /// before it builds, so the test places the shipped file first to exercise the real rules. + /// + static async Task ApplyShippedStylePolicyAsync(ProjectHarness harness, CancellationToken cancellationToken) + { + var shippedEditorConfig = await harness.GetPropertyAsync("EditorConfigFilePath", cancellationToken); + + await File.WriteAllTextAsync( + Path.Combine(harness.SolutionDirectory, ".editorconfig"), + await File.ReadAllTextAsync(shippedEditorConfig, cancellationToken), + cancellationToken + ); + } + + static async Task WriteProbeAsync(ProjectHarness harness, string fieldName, CancellationToken cancellationToken) + { + var source = $$""" + namespace Test.MyApp; + + sealed class Probe + { + int {{fieldName}}; + + public int Read() => {{fieldName}}; + } + """; + + await File.WriteAllTextAsync(Path.Combine(harness.ProjectDirectory, "Probe.cs"), source, cancellationToken); + } +} diff --git a/src/tests/BuildSdk.IntegrationTests/GeneratedAssemblyInfoTests.cs b/src/tests/BuildSdk.IntegrationTests/GeneratedAssemblyInfoTests.cs index 176900f..6e66d63 100644 --- a/src/tests/BuildSdk.IntegrationTests/GeneratedAssemblyInfoTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/GeneratedAssemblyInfoTests.cs @@ -7,7 +7,7 @@ namespace Purview.BuildSdk; /// Verifies the auto-generated AssemblyInfo class handles property values containing /// MSBuild item-list separators (semicolons) without producing broken C#. /// -public sealed class GeneratedAssemblyInfoTests +sealed class GeneratedAssemblyInfoTests { [Test] public async Task GeneratedAssemblyInfo_WithSemicolonInProperties_BuildsAndPreservesValues( diff --git a/src/tests/BuildSdk.IntegrationTests/InternalsVisibleToTests.cs b/src/tests/BuildSdk.IntegrationTests/InternalsVisibleToTests.cs index 688c199..fa8582e 100644 --- a/src/tests/BuildSdk.IntegrationTests/InternalsVisibleToTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/InternalsVisibleToTests.cs @@ -6,7 +6,7 @@ namespace Purview.BuildSdk; /// Verifies that the SDK correctly generates InternalsVisibleToAttribute entries for test types /// and shared testing projects, and that this behaviour can be disabled. /// -public sealed class InternalsVisibleToTests +sealed class InternalsVisibleToTests { [Test] public async Task InternalsVisibleTo_Generated_For_NonTestProjects_ByDefault(CancellationToken cancellationToken) diff --git a/src/tests/BuildSdk.IntegrationTests/IsCLIProjectTests.cs b/src/tests/BuildSdk.IntegrationTests/IsCLIProjectTests.cs index dfb714b..de8c359 100644 --- a/src/tests/BuildSdk.IntegrationTests/IsCLIProjectTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/IsCLIProjectTests.cs @@ -8,7 +8,7 @@ namespace Purview.BuildSdk; /// Projects ending with CLI, Console, or CommandLine (case-insensitive) should be classified /// as CLI projects. /// -public sealed class IsCLIProjectTests +sealed class IsCLIProjectTests { /// /// Helper to create a project and evaluate its IsCLIProject property. @@ -109,7 +109,7 @@ CancellationToken cancellationToken } [Test] - public async Task ProjectCLI_HasNoWarnCA1515(CancellationToken cancellationToken) + public async Task ProjectCLI_DoesNotSilenceAccessibilityRules(CancellationToken cancellationToken) { var (harness, _) = await CreateProjectAndEvaluateAsync("MyCLI", cancellationToken); @@ -117,8 +117,11 @@ public async Task ProjectCLI_HasNoWarnCA1515(CancellationToken cancellationToken { var properties = await harness.GetPropertiesAsync(cancellationToken, "NoWarn"); + // CLI applications are executables, so public types are flagged by CA1515; the fix is to + // make them internal, never to silence the rule through NoWarn. await Assert.That(properties).ContainsKey("NoWarn"); - await Assert.That(properties["NoWarn"]).Contains("CA1515"); + await Assert.That(properties["NoWarn"]).DoesNotContain("CA1515"); + await Assert.That(properties["NoWarn"]).DoesNotContain("IDE0040"); } } diff --git a/src/tests/BuildSdk.IntegrationTests/PackableProjectDefaultsTests.cs b/src/tests/BuildSdk.IntegrationTests/PackableProjectDefaultsTests.cs index 2df2024..b2ddb75 100644 --- a/src/tests/BuildSdk.IntegrationTests/PackableProjectDefaultsTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/PackableProjectDefaultsTests.cs @@ -13,7 +13,7 @@ namespace Purview.BuildSdk; /// auto-inclusion works when present, and solution-wide packs skip non-packable projects without /// warnings unless explicitly opted in. /// -public sealed class PackableProjectDefaultsTests +sealed class PackableProjectDefaultsTests { const string OfflinePackProps = "true" diff --git a/src/tests/BuildSdk.IntegrationTests/PackageBehaviorTests.cs b/src/tests/BuildSdk.IntegrationTests/PackageBehaviorTests.cs index 3d5af3a..3dcdf61 100644 --- a/src/tests/BuildSdk.IntegrationTests/PackageBehaviorTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/PackageBehaviorTests.cs @@ -6,7 +6,7 @@ namespace Purview.BuildSdk; /// Verifies SDK-controlled package and packaging behaviour — IsPackable defaults, /// SourceLink opt-in/opt-out, and telemetry exclusion flags. /// -public sealed class PackagebehaviourTests +sealed class PackagebehaviourTests { [Test] public async Task Library_IsPackable_False_ByDefault(CancellationToken cancellationToken) diff --git a/src/tests/BuildSdk.IntegrationTests/ProjectClassificationTests.cs b/src/tests/BuildSdk.IntegrationTests/ProjectClassificationTests.cs index 4d825b2..ed82fcb 100644 --- a/src/tests/BuildSdk.IntegrationTests/ProjectClassificationTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/ProjectClassificationTests.cs @@ -6,7 +6,7 @@ namespace Purview.BuildSdk; /// Verifies that Sdk.props correctly classifies project types from file names, Sdk attributes, /// and the presence of a Dockerfile — all without triggering a build. /// -public sealed class ProjectClassificationTests +sealed class ProjectClassificationTests { [Test] public async Task CSharpProject_IsCSharpProject_True(CancellationToken cancellationToken) diff --git a/src/tests/BuildSdk.IntegrationTests/ProjectIdentityRulesTests.cs b/src/tests/BuildSdk.IntegrationTests/ProjectIdentityRulesTests.cs index 4099048..a2216ea 100644 --- a/src/tests/BuildSdk.IntegrationTests/ProjectIdentityRulesTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/ProjectIdentityRulesTests.cs @@ -5,7 +5,7 @@ namespace Purview.BuildSdk; -public sealed class ProjectIdentityRulesTests +sealed class ProjectIdentityRulesTests { [Test] public async Task RootProject_UsesNamespacePrefixAsIs(CancellationToken cancellationToken) diff --git a/src/tests/BuildSdk.IntegrationTests/RepositoryFileSyncTests.cs b/src/tests/BuildSdk.IntegrationTests/RepositoryFileSyncTests.cs index 2d50d03..e6deeae 100644 --- a/src/tests/BuildSdk.IntegrationTests/RepositoryFileSyncTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/RepositoryFileSyncTests.cs @@ -14,7 +14,7 @@ namespace Purview.BuildSdk; /// default, or a warning when the caller opts out, /// * bootstrap files are never overwritten unless explicitly requested. /// -public sealed class RepositoryFileSyncTests +sealed class RepositoryFileSyncTests { const string AgentSourceFolder = "agent-source"; const string AgentsFolder = ".agents"; @@ -287,6 +287,130 @@ public async Task RepoBootstrapModeNever_SkipsAllBootstrapWrites(CancellationTok await Assert.That(File.Exists(Path.Combine(h.SolutionDirectory, ".editorconfig"))).IsFalse(); } + [Test] + public async Task StylePolicy_DisabledInEditorConfig_FailsTheBuild(CancellationToken cancellationToken) + { + using var h = await CreateHarnessAsync(extraProps: null, cancellationToken: cancellationToken); + + var editorConfigPath = Path.Combine(h.SolutionDirectory, ".editorconfig"); + await File.WriteAllTextAsync( + editorConfigPath, + "[*.cs]\ndotnet_diagnostic.IDE0040.severity = none\n", + cancellationToken + ); + + var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken); + + await Assert.That(success).IsFalse(); + await Assert.That(output + errors).Contains("PRSGD0007"); + } + + [Test] + public async Task StylePolicy_ModifierOptionOverride_FailsTheBuild(CancellationToken cancellationToken) + { + using var h = await CreateHarnessAsync(extraProps: null, cancellationToken: cancellationToken); + + var editorConfigPath = Path.Combine(h.SolutionDirectory, ".editorconfig"); + await File.WriteAllTextAsync( + editorConfigPath, + "[*.cs]\ndotnet_style_require_accessibility_modifiers = always\n", + cancellationToken + ); + + var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken); + + await Assert.That(success).IsFalse(); + await Assert.That(output + errors).Contains("PRSGD0006"); + } + + [Test] + public async Task StylePolicy_SuppressedThroughNoWarn_FailsTheBuild(CancellationToken cancellationToken) + { + using var h = await CreateHarnessAsync( + extraProps: "$(NoWarn);CA1515", + cancellationToken: cancellationToken + ); + + var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken); + + await Assert.That(success).IsFalse(); + await Assert.That(output + errors).Contains("PRSGD0008"); + } + + [Test] + public async Task StylePolicy_ValidationCanBeDisabled(CancellationToken cancellationToken) + { + using var h = await CreateHarnessAsync( + extraProps: "true", + cancellationToken: cancellationToken + ); + + var editorConfigPath = Path.Combine(h.SolutionDirectory, ".editorconfig"); + await File.WriteAllTextAsync( + editorConfigPath, + "[*.cs]\ndotnet_diagnostic.IDE0040.severity = none\n", + cancellationToken + ); + + var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken); + + await Assert.That(success).IsTrue().Because(TestHelpers.GenerateError(output, errors)); + } + + [Test] + public async Task StylePolicy_NamingDiagnosticHidden_FailsTheBuild(CancellationToken cancellationToken) + { + using var h = await CreateHarnessAsync(extraProps: null, cancellationToken: cancellationToken); + + var editorConfigPath = Path.Combine(h.SolutionDirectory, ".editorconfig"); + await File.WriteAllTextAsync( + editorConfigPath, + "[*.cs]\ndotnet_diagnostic.IDE1006.severity = silent\n", + cancellationToken + ); + + var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken); + + await Assert.That(success).IsFalse(); + await Assert.That(output + errors).Contains("PRSGD0009"); + } + + [Test] + public async Task StylePolicy_FieldNamingRuleDisabled_FailsTheBuild(CancellationToken cancellationToken) + { + using var h = await CreateHarnessAsync(extraProps: null, cancellationToken: cancellationToken); + + var editorConfigPath = Path.Combine(h.SolutionDirectory, ".editorconfig"); + await File.WriteAllTextAsync( + editorConfigPath, + "[*.cs]\ndotnet_naming_rule.private_instance_fields_must_be_camel_case_with_underscore_prefix.severity = none\n", + cancellationToken + ); + + var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken); + + await Assert.That(success).IsFalse(); + await Assert.That(output + errors).Contains("PRSGD0009"); + } + + [Test] + public async Task StylePolicy_FieldPrefixRemoved_FailsTheBuild(CancellationToken cancellationToken) + { + using var h = await CreateHarnessAsync(extraProps: null, cancellationToken: cancellationToken); + + var editorConfigPath = Path.Combine(h.SolutionDirectory, ".editorconfig"); + await File.WriteAllTextAsync( + editorConfigPath, + "[*.cs]\ndotnet_naming_style.camel_case_underscore_style.required_prefix =\n", + cancellationToken + ); + + var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken); + + await Assert.That(success).IsFalse(); + await Assert.That(output + errors).Contains("PRSGD0009"); + } + [Test] public async Task CopyRetryWarnings_AreDemotedByDefaultAndRestorable(CancellationToken cancellationToken) { diff --git a/src/tests/BuildSdk.IntegrationTests/RootNamespaceNamingTests.cs b/src/tests/BuildSdk.IntegrationTests/RootNamespaceNamingTests.cs index 4e10ab3..b432263 100644 --- a/src/tests/BuildSdk.IntegrationTests/RootNamespaceNamingTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/RootNamespaceNamingTests.cs @@ -7,7 +7,7 @@ namespace Purview.BuildSdk; /// evaluated RootNamespace, explicit overrides always win, and no literal $(...) expression /// survives into either name. /// -public sealed class RootNamespaceNamingTests +sealed class RootNamespaceNamingTests { [Test] public async Task PackableLibrary_AssemblyNameAndPackageId_DefaultToRootNamespace( diff --git a/src/tests/BuildSdk.IntegrationTests/RoslynComponentDefaultsTests.cs b/src/tests/BuildSdk.IntegrationTests/RoslynComponentDefaultsTests.cs index 4fe8962..e0cfb30 100644 --- a/src/tests/BuildSdk.IntegrationTests/RoslynComponentDefaultsTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/RoslynComponentDefaultsTests.cs @@ -7,7 +7,7 @@ namespace Purview.BuildSdk; /// /// Verifies the source-generator defaults applied to Roslyn component projects. /// -public sealed class RoslynComponentDefaultsTests +sealed class RoslynComponentDefaultsTests { [Test] public async Task RepositoryTargetFramework_DoesNotOverrideRoslynComponentDefault( diff --git a/src/tests/BuildSdk.IntegrationTests/SdkPackageConsumptionTests.cs b/src/tests/BuildSdk.IntegrationTests/SdkPackageConsumptionTests.cs index 80ea12a..22a0db1 100644 --- a/src/tests/BuildSdk.IntegrationTests/SdkPackageConsumptionTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/SdkPackageConsumptionTests.cs @@ -10,7 +10,7 @@ namespace Purview.BuildSdk; /// Proves the packed SDK can be consumed from a local NuGet feed and that its .editorconfig /// is resolved and applied as an EditorConfigFiles entry in a fresh consumer project. /// -public sealed class SdkPackageConsumptionTests +sealed class SdkPackageConsumptionTests { // Both tests in this class build the shared BuildSdk/Analyzers project in place via `dotnet pack`; // serialize them to avoid concurrent CSC file-lock conflicts on the same obj/bin outputs. @@ -533,6 +533,58 @@ await Assert .Because( "The Extensions section must suppress namespace-conflict diagnostics so no pragmas are required." ); + await Assert + .That(editorConfig) + .Contains("dotnet_style_require_accessibility_modifiers = omit_if_default:warning") + .Because( + "The modifier policy must ship as omit_if_default: a declared accessibility modifier that equals the language default is noise and must be removed." + ); + await Assert + .That(editorConfig) + .Contains("dotnet_diagnostic.CA1515.severity = warning") + .Because("CA1515 must ship at warning so over-visible types cannot be ignored."); + await Assert + .That(editorConfig) + .DoesNotContain("dotnet_code_quality.require_accessibility_modifiers") + .Because("Unsupported 'dotnet_code_quality' keys are silently ignored and must not ship."); + await Assert + .That(editorConfig) + .DoesNotContain("ignore_internalsvisibleto") + .Because("CA1852 must not be suppressed for types exposed through InternalsVisibleTo."); + await Assert + .That(editorConfig.Split("dotnet_diagnostic.IDE0040.severity").Length - 1) + .IsEqualTo(1) + .Because("IDE0040 must be declared exactly once so its effective severity is unambiguous."); + await Assert + .That(editorConfig) + .Contains( + "dotnet_naming_rule.private_instance_fields_must_be_camel_case_with_underscore_prefix.severity = warning" + ) + .Because("Private instance fields must be enforced as '_camelCase'."); + await Assert + .That(editorConfig) + .Contains("dotnet_naming_style.camel_case_underscore_style.required_prefix = _") + .Because("The '_' prefix is what keeps field access free of the noisy 'this.' qualifier."); + await Assert + .That(editorConfig) + .Contains("dotnet_diagnostic.IDE1006.severity = warning") + .Because("Naming rules are only reported while the naming diagnostic is visible."); + await Assert + .That(editorConfig) + .Contains("dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions") + .Because("A catch-all symbol group makes the local-function rule demand PascalCase everywhere."); + await Assert + .That(editorConfig) + .DoesNotContain("all_members") + .Because("The catch-all symbol group must not ship: it flags locals, fields and parameters."); + await Assert + .That(editorConfig) + .DoesNotContain("dotnet_naming_rule.private_fields.severity") + .Because("A rule that references undefined styles/symbol groups enforces nothing."); + await Assert + .That(editorConfig) + .DoesNotContain("dotnet_naming_rule.types_should_be_pascal_case.severity") + .Because("A rule that references undefined styles/symbol groups enforces nothing."); } } diff --git a/src/tests/BuildSdk.IntegrationTests/TestTypeDetectionTests.cs b/src/tests/BuildSdk.IntegrationTests/TestTypeDetectionTests.cs index fee0245..d1ce7af 100644 --- a/src/tests/BuildSdk.IntegrationTests/TestTypeDetectionTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/TestTypeDetectionTests.cs @@ -2,7 +2,7 @@ namespace Purview.BuildSdk; -public class TestTypeDetectionTests +class TestTypeDetectionTests { [Test] [MethodDataSource(nameof(TestTypes))] diff --git a/src/tests/BuildSdk.IntegrationTests/TestWiringTests.cs b/src/tests/BuildSdk.IntegrationTests/TestWiringTests.cs index 7861d1b..31a00f0 100644 --- a/src/tests/BuildSdk.IntegrationTests/TestWiringTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/TestWiringTests.cs @@ -6,7 +6,7 @@ namespace Purview.BuildSdk; /// Verifies that the SDK wires the correct test framework packages and output type /// for projects that match the test-project naming convention. /// -public sealed class TestWiringTests +sealed class TestWiringTests { [Test] public async Task TestProject_DefaultFrameworks_AreTUnitTUnitMocksAndBogus(CancellationToken cancellationToken) @@ -25,6 +25,21 @@ public async Task TestProject_DefaultFrameworks_AreTUnitTUnitMocksAndBogus(Cance await Assert.That(eval.Properties["TestDataFramework"]).IsEqualTo("Bogus"); } + [Test] + public async Task TestProject_DoesNotSilenceAccessibilityRules(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync("MyApp.UnitTests", cancellationToken: cancellationToken); + + var noWarn = await h.GetPropertyAsync("NoWarn", cancellationToken); + + // Test projects are executables, so the accessibility rules are reported for them; test + // classes must be non-public instead of the rules being suppressed through NoWarn. + await Assert.That(noWarn).DoesNotContain("CA1515"); + await Assert.That(noWarn).DoesNotContain("CA1062"); + await Assert.That(noWarn).DoesNotContain("CA1707"); + await Assert.That(noWarn).DoesNotContain("IDE0040"); + } + [Test] public async Task TestProject_Xunit_WhenOptedIn(CancellationToken cancellationToken) { diff --git a/src/tests/BuildSdk.IntegrationTests/VersionDetectionTests.cs b/src/tests/BuildSdk.IntegrationTests/VersionDetectionTests.cs index c7e38f8..eefa1b8 100644 --- a/src/tests/BuildSdk.IntegrationTests/VersionDetectionTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/VersionDetectionTests.cs @@ -6,7 +6,7 @@ namespace Purview.BuildSdk; /// Verifies SDK version detection from package.json: property defaults, explicit and /// auto-discovered package.json paths, opt-out behaviour, and build-time validation errors. /// -public sealed partial class VersionDetectionTests +sealed partial class VersionDetectionTests { [Test] public async Task UsePackageJsonVersion_DefaultsToTrue(CancellationToken cancellationToken)