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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Nota.CodeAnalysis.Verification/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,18 @@ rule per analyser - plus a deliberately mis-encoded file for `NOTA0001`, which p
the only rule with an opt-out that defaults to enforcing and so the only one where a wrong default
would ship as silence. It fails on `CS9057` too, since that is a warning nothing else would notice.

It also asserts the opposite of everything above: that `NOTA0001` and `NOTA0002` report the
consumer's own files and **nothing else**. The throwaway project references `Microsoft.NET.Test.Sdk`
purely for what that drags in - a source file of its own, from the read-only NuGet cache, carrying a
UTF-8 byte order mark. Every test project on earth compiles that file, and the encoding check
reported it on all of them until it learned to skip what the consumer did not write.

A false positive there is worse than a missing check, which is why it is asserted rather than left to
notice: the file cannot be re-saved, it is restored the moment it is touched, and it is shared by
every project on the machine. The only way out was `NotaValidateSourceEncoding=false`, which throws
away `NOTA0001` as well - so a rule meant to catch corruption talked people into switching off the
one thing standing between them and it.

It packs under a throwaway version like `0.0.0-verify.20260802143000`. That is not cosmetic: NuGet
extracts a package once per version into the global cache, so re-packing `2.2.0` and installing
`2.2.0` gets whatever was extracted first, and the change under test never reaches the consumer. This
Expand Down
17 changes: 17 additions & 0 deletions Nota.CodeAnalysis.Verification/verify-package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ cat > "$app/App.csproj" <<EOF
<PackageReference Include="Nota.CodeAnalysis" Version="$version" />
<!-- SerilogAnalyzer has nothing to say without Serilog present. -->
<PackageReference Include="Serilog" Version="4.2.0" />
<!-- Here for what it drags in rather than what it does: it contributes a source file of its own
to @(Compile), from the read-only NuGet cache, and that file carries a UTF-8 byte order
mark. NOTA0002 reported it on every test project in existence until the encoding check
learned to skip files the consumer did not write and cannot re-save. -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
</ItemGroup>
</Project>
EOF
Expand Down Expand Up @@ -155,6 +160,18 @@ for rule in $expected; do
fi
done

# NOTA0001 and NOTA0002 must report the consumer's own files and nothing else. A file inside the
# NuGet cache is not the consumer's to fix - it is shared, read-only, and restored again the moment
# it is touched - so reporting one leaves switching the whole check off as the only way to a clean
# build. Checked by name rather than by count: the rules are expected to fire above, so a count says
# nothing about which file they fired on.
foreign="$(printf '%s' "$output" | grep -E '(warning|error) NOTA000[0-9]' | grep -iE '[/\\]\.nuget[/\\]|[/\\]packages[/\\]' || true)"
if [ -n "$foreign" ]; then
printf '\nThe encoding check reported a file the consumer cannot fix:\n%s\n' "$foreign" >&2
printf 'It lives in the NuGet cache. Skip it rather than asking anyone to re-save it.\n\n' >&2
exit 1
fi

# CS9057 is never acceptable: it means an analyser referenced a newer compiler than the one running,
# and was skipped. It is a warning, so nothing else would fail.
if printf '%s' "$output" | grep -q "CS9057"; then
Expand Down
44 changes: 43 additions & 1 deletion Nota.CodeAnalysis/build/Nota.CodeAnalysis.targets
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,22 @@
<ParameterGroup>
<Files ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
<AllowByteOrderMark ParameterType="System.Boolean" Required="false" />
<SkipRoots ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" />
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="cs"><![CDATA[
// Directories whose contents are not this repository's to fix. Normalised once, with a
// trailing separator, so that a prefix match cannot let "obj" swallow "objects".
var skip = new System.Collections.Generic.List<string>();
foreach (var root in SkipRoots ?? new Microsoft.Build.Framework.ITaskItem[0])
{
var full = root.GetMetadata("FullPath");
if (!string.IsNullOrEmpty(full))
{
skip.Add(full.TrimEnd(System.IO.Path.DirectorySeparatorChar) + System.IO.Path.DirectorySeparatorChar);
}
}

foreach (var item in Files)
{
var path = item.GetMetadata("FullPath");
Expand All @@ -60,6 +73,27 @@
continue;
}

// A package's own sources are compiled into this project but belong to whoever shipped
// them: Microsoft.NET.Test.Sdk contributes a marked file from the NuGet cache, and no
// consumer can re-save a file in a shared read-only cache. Generated files under obj go
// the same way - they are rewritten on the next build, so a report is noise that cannot
// be acted on. Reporting either would leave switching the whole check off as the only
// way to get a clean build, which is worse than not checking those files.
var skipped = false;
foreach (var root in skip)
{
if (path.StartsWith(root, System.StringComparison.OrdinalIgnoreCase))
{
skipped = true;
break;
}
}

if (skipped)
{
continue;
}

var bytes = System.IO.File.ReadAllBytes(path);

// UTF-16 carrying a BOM is fine: the compiler reads it correctly, and svcutil and EF
Expand Down Expand Up @@ -120,7 +154,15 @@
<Target Name="NotaValidateSourceEncoding"
BeforeTargets="CoreCompile"
Condition="'$(NotaValidateSourceEncoding)' == 'true' AND '@(Compile)' != ''">
<NotaValidateUtf8 Files="@(Compile)" AllowByteOrderMark="$(NotaAllowUtf8Bom)" />
<ItemGroup>
<!-- NuGetPackageRoot is empty in some restore-less contexts, and an empty root would match
every path and check nothing, so it is only added when it has a value. -->
<NotaEncodingSkipRoot Include="$(NuGetPackageRoot)" Condition="'$(NuGetPackageRoot)' != ''" />
<NotaEncodingSkipRoot Include="$(IntermediateOutputPath)" Condition="'$(IntermediateOutputPath)' != ''" />
</ItemGroup>

<NotaValidateUtf8 Files="@(Compile)" AllowByteOrderMark="$(NotaAllowUtf8Bom)"
SkipRoots="@(NotaEncodingSkipRoot)" />
</Target>

</Project>
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ Most of this is unsurprising. These are the ones that catch people out:
costs nothing at runtime, so a tree that has them should say so on every build rather than be
unbuildable until someone sweeps it. Which of the several ways to promote a warning actually
promotes this one is not obvious - see below. UTF-16 is exempt, as under `NOTA0001`.
- **Both only look at files you wrote.** Sources under the NuGet cache and generated sources under
`obj` are compiled into your project but are not yours to re-save, so neither rule reports them.
`Microsoft.NET.Test.Sdk` is the one that made this necessary: it contributes a marked file of its
own to every test project, from a read-only shared cache that restores it the moment it is touched.
Reporting that left `NotaValidateSourceEncoding=false` as the only route to a clean build, which
throws away `NOTA0001` too - a rule against corruption talking people into switching off the only
guard against it.
- **`UA1000` and `UA1001`** enforce the using layout: System, then third party, then yours, as blocks
separated by a blank line, one run per vendor. An existing repository is converted in one pass with
`dotnet format analyzers --diagnostics UA1000 UA1001 --severity warn`.
Expand Down