diff --git a/CHANGELOG.md b/CHANGELOG.md index 6deed4d..84594a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project are documented here. The format follows [Kee ### Added - Every warning and error links to its section of the [code reference](./docs/reference/codes.md) (`HelpLink`, shown by the terminal logger and IDEs). `MSKit_CodesHelpBaseUrl` points the links at another copy of the page. +- A machine-readable catalog of the codes: every part declares each code it reports as a `BuildDiagnosticDescriptor` item (`Title`, `MessageFormat`, `Description`, `Category`, `DefaultSeverity`, `HelpLink`) in its own `diagnostic.descriptors.props`, so a tool can read them with `dotnet msbuild -getItem:BuildDiagnosticDescriptor` and tell the owning part from the item's `DefiningProjectFullPath`. Each section of the [code reference](./docs/reference/codes.md) now opens with the code's title. See the [diagnostic catalog](./docs/reference/diagnostic-catalog.md). - `MSKit_DefaultPackageIconUrl`: when the kit packs its own icon, this URL is also written as `PackageIconUrl`, so clients that predate embedded icons show it; nuget.org keeps showing the embedded one. Empty by default; an owner sets it in its owner layer. `MSKITPKG008` now reports only a `PackageIconUrl` the project sets itself. See [docs/packaging.md](./docs/packaging.md#package-metadata). - `manager/`: the first build of `mskit-manager`, the `DragoAnt.MSBuildKit.Manager` .NET tool (`net8.0`, `net10.0`, `RollForward=Major`) that will install, update and migrate the kit. This build has one command, `status [--json]`, which prints the tool version; the logo goes to stderr, only on a terminal and never with `--no-logo`, so `--json` output always parses. Each run writes a log under `/mskit-manager/logs/`, named after the command, newest 20 kept. Not published yet. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf87858..d7891f3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ Both scripts restore into a global-packages folder of their own, `dist/selftest- ## Changing the kit - A new property defaults with `Condition="'$(Name)'==''"`, so a consumer's value always wins, and gets a row in [docs/reference/properties.md](./docs/reference/properties.md) plus a mention on its topic page. -- A new check gets an `MSKIT` code with no separator (a shipped code is never renumbered or reused), a `HelpLink="$(MSKit_CodesHelpBaseUrl)#"`, a message that says how to fix it, a section in [docs/reference/codes.md](./docs/reference/codes.md) headed by the code, a fixture that triggers it and a line in `tests/run.sh`. +- A new check gets an `MSKIT` code with no separator (a shipped code is never renumbered or reused), a `HelpLink="$(MSKit_CodesHelpBaseUrl)#"`, a message that says how to fix it, a section in [docs/reference/codes.md](./docs/reference/codes.md) headed by the code, a `BuildDiagnosticDescriptor` item in its part's `diagnostic.descriptors.props` ([diagnostic catalog](./docs/reference/diagnostic-catalog.md)), a fixture that triggers it and a line in `tests/run.sh`. - `sh tools/docs-check.sh` fails on a property, item or code without its reference entry, on a name the docs mention that the kit lacks, and on a broken relative link; `--list properties|items|codes` prints the kit's inventory with the file and line of each. - The README stays short: key features, install, links. Detail goes to the topic page in `docs/`. - A new part needs a line in `kit/.toolkit/kit.parts` and its `init.props` / `init.targets` imports in the entry points. diff --git a/docs/README.md b/docs/README.md index b3055a7..e57cd62 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,7 @@ Read in this order; each page stands on its own, so jump to the one you need. | 13 | [Troubleshooting](./troubleshooting.md) | fix a failing build or update | | 14 | [Code reference](./reference/codes.md) | look up any warning or error the kit reports | | 15 | [Property reference](./reference/properties.md) | look up any property the kit sets or reads | -| 16 | [Migrating from MSBuild.Routine](./migrating-from-msbuild-routine.md) | move a repository off the older submodule | +| 16 | [Diagnostic catalog](./reference/diagnostic-catalog.md) | read the kit's codes from a tool, or add your own to the catalog | +| 17 | [Migrating from MSBuild.Routine](./migrating-from-msbuild-routine.md) | move a repository off the older submodule | Contributing to the kit itself: [CONTRIBUTING.md](../CONTRIBUTING.md). diff --git a/docs/parts.md b/docs/parts.md index 0f92001..04fba74 100644 --- a/docs/parts.md +++ b/docs/parts.md @@ -27,7 +27,7 @@ The kit is split into parts, one folder each under `.toolkit/msbuild/` (`DragoAn | Phase | Order | | --- | --- | -| props | `MSKit_BeforeInitProps` → owner layer (`init.company.props`) → Core → Vcs.GitHub → Locals.Compile → Locals.DirectorySecrets → Locals.Secrets → Project.RoslynComponent → Project.CodeFixer → Project.CodeAnalyzer → Project.SourceGenerator → TfmConstants → Trunk → Packaging → Testing → Testing.XUnit.v3 → EF → Trunk `init.last.props` (version engine) → `MSKit_AfterInitProps` | +| props | `MSKit_BeforeInitProps` → owner layer (`init.company.props`) → Core → Vcs.GitHub → Locals.Compile → Locals.DirectorySecrets → Locals.Secrets → Project.RoslynComponent → Project.CodeFixer → Project.CodeAnalyzer → Project.SourceGenerator → TfmConstants → Trunk → Packaging → Testing → Testing.XUnit.v3 → EF → PackageAsProj → Trunk `init.last.props` (version engine) → `MSKit_AfterInitProps` | | targets | `MSKit_BeforeInitTargets` → TfmConstants → owner layer (`init.company.targets`) → Core → Locals.* → Project.CodeAnalyzer → Project.SourceGenerator → Trunk → Packaging → Testing → Testing.XUnit.v3 → the `init.last.targets` of Trunk, Project.RoslynComponent, Testing.XUnit.v3, Testing, Project.CodeAnalyzer, PrivateAssets, PackageAsProj, ProjMetadata → every part's `audit/*.targets` → `MSKit_AfterInitTargets` | In the props phase a default is written as ``, so the **first** writer wins: a value you set in `Directory.Build.props` above the kit import beats the owner layer, which beats the parts. The exceptions are the owner layer's `ManufacturerName`, `FullManufacturerName` and `NoWarn`, which it sets unconditionally ([Customizing](./customizing.md#the-owner-layer)). The csproj body runs after all props, so a value set there wins too, except for the few properties the kit reads in the props phase (the test-project switches, `TargetFramework` detection); those pages say so. How to hook in your own files: [Customizing](./customizing.md). diff --git a/docs/reference/codes.md b/docs/reference/codes.md index e5add78..a30b3df 100644 --- a/docs/reference/codes.md +++ b/docs/reference/codes.md @@ -1,6 +1,6 @@ # Code reference -Every warning and error the kit reports, one section per code. A code is the kit prefix, a family and a three-digit number with no separator (`MSKITVER006`); its section anchor is the code in lower case (`#mskitver006`). Every warning and error carries a `HelpLink` to its section, which the terminal logger and IDEs show as a link on the code; `MSKit_CodesHelpBaseUrl` names the page the links point at ([property reference](./properties.md#machine-ci-and-paths)). Severity is the default; the section says how to change or skip it. +Every warning and error the kit reports, one section per code. A code is the kit prefix, a family and a three-digit number with no separator (`MSKITVER006`); its section anchor is the code in lower case (`#mskitver006`). Every warning and error carries a `HelpLink` to its section, which the terminal logger and IDEs show as a link on the code; `MSKit_CodesHelpBaseUrl` names the page the links point at ([property reference](./properties.md#machine-ci-and-paths)). Each section opens with the code's short title and names its default severity; the section says how to change or skip it. Tools read the same title, severity and opening sentence from the kit itself, as `BuildDiagnosticDescriptor` items ([diagnostic catalog](./diagnostic-catalog.md)). Earlier releases put an underscore between the prefix and the family; each section names its former spelling, and `NoWarn`, `WarningsAsErrors`, `WarningsNotAsErrors` or `MSKit_SkipPackageChecks` entries that use it no longer match. A code is never renumbered or reused: `VER003` is reserved, and no release reported it. @@ -20,90 +20,134 @@ Earlier releases put an underscore between the prefix and the family; each secti ### MSKITPKG001 -`MSKITPKG001` (formerly `MSKIT_PKG001`) — the package has no real `Description`: it is empty, the SDK's `Package Description`, the package id or the project name. Write one or two sentences on what the package does and what sets it apart; nuget.org search shows it first. +**Package has no real `Description`** + +`MSKITPKG001` (formerly `MSKIT_PKG001`) (warning, error on CI) — the package has no real `Description`: it is empty, the SDK's `Package Description`, the package id or the project name. Write one or two sentences on what the package does and what sets it apart; nuget.org search shows it first. ### MSKITPKG002 -`MSKITPKG002` (formerly `MSKIT_PKG002`) — `Description` is shorter than `MSKit_PackageDescriptionMinLength` (30 characters). Say what it does and for whom, or lower the bar. +**Package `Description` is too short** + +`MSKITPKG002` (formerly `MSKIT_PKG002`) (warning, error on CI) — `Description` is shorter than `MSKit_PackageDescriptionMinLength` (30 characters). Say what it does and for whom, or lower the bar. ### MSKITPKG003 -`MSKITPKG003` (formerly `MSKIT_PKG003`) — no README is packed. Add `package.readme.md` (or `README.md`) next to the csproj, or set `MSKit_PackageReadmeFrom`. +**Package has no README** + +`MSKITPKG003` (formerly `MSKIT_PKG003`) (warning, error on CI) — no README is packed. Add `package.readme.md` (or `README.md`) next to the csproj, or set `MSKit_PackageReadmeFrom`. ### MSKITPKG004 -`MSKITPKG004` (formerly `MSKIT_PKG004`) — no `PackageTags`. Add a few search terms that are not already in the package id. +**Package has no `PackageTags`** + +`MSKITPKG004` (formerly `MSKIT_PKG004`) (warning, error on CI) — no `PackageTags`. Add a few search terms that are not already in the package id. ### MSKITPKG005 -`MSKITPKG005` (formerly `MSKIT_PKG005`) — no icon. Set `PackageIconPath` to a 128×128 PNG or JPEG, packed as `icon`; the owner layer sets one for every package. +**Package has no icon** + +`MSKITPKG005` (formerly `MSKIT_PKG005`) (warning, error on CI) — no icon. Set `PackageIconPath` to a 128×128 PNG or JPEG, packed as `icon`; the owner layer sets one for every package. ### MSKITPKG006 -`MSKITPKG006` (formerly `MSKIT_PKG006`) — no licence. Set `PackageLicenseExpression` to an [SPDX id](https://spdx.org/licenses/) or `PackageLicenseFile`. +**Package has no licence** + +`MSKITPKG006` (formerly `MSKIT_PKG006`) (warning, error on CI) — no licence. Set `PackageLicenseExpression` to an [SPDX id](https://spdx.org/licenses/) or `PackageLicenseFile`. ### MSKITPKG007 -`MSKITPKG007` (formerly `MSKIT_PKG007`) — the deprecated `PackageLicenseUrl` is set. Use `PackageLicenseExpression` or `PackageLicenseFile`. +**`PackageLicenseUrl` is deprecated** + +`MSKITPKG007` (formerly `MSKIT_PKG007`) (warning, error on CI) — the deprecated `PackageLicenseUrl` is set. Use `PackageLicenseExpression` or `PackageLicenseFile`. ### MSKITPKG008 -`MSKITPKG008` (formerly `MSKIT_PKG008`) — the project sets the deprecated `PackageIconUrl`. Pack the image and use `PackageIcon` or `PackageIconPath`. To keep a URL for older clients, set `MSKit_DefaultPackageIconUrl` instead: the kit writes it only next to its own embedded icon, and that one is not reported. +**`PackageIconUrl` is deprecated** + +`MSKITPKG008` (formerly `MSKIT_PKG008`) (warning, error on CI) — the project sets the deprecated `PackageIconUrl`. Pack the image and use `PackageIcon` or `PackageIconPath`. To keep a URL for older clients, set `MSKit_DefaultPackageIconUrl` instead: the kit writes it only next to its own embedded icon, and that one is not reported. ### MSKITPKG009 -`MSKITPKG009` (formerly `MSKIT_PKG009`) — the package README has relative images, which nuget.org does not render. Use absolute `https` URLs from an [allowed host](https://learn.microsoft.com/nuget/nuget-org/package-readme-on-nuget-org#allowed-domains-for-images-and-badges), or generate the readme with `MSKit_PackageReadmeFrom`. +**Package README has relative images** + +`MSKITPKG009` (formerly `MSKIT_PKG009`) (warning, error on CI) — the package README has relative images, which nuget.org does not render. Use absolute `https` URLs from an [allowed host](https://learn.microsoft.com/nuget/nuget-org/package-readme-on-nuget-org#allowed-domains-for-images-and-badges), or generate the readme with `MSKit_PackageReadmeFrom`. ### MSKITPKG010 -`MSKITPKG010` (formerly `MSKIT_PKG010`) — the package README contains HTML, which nuget.org does not render. Use Markdown. +**Package README contains HTML** + +`MSKITPKG010` (formerly `MSKIT_PKG010`) (warning, error on CI) — the package README contains HTML, which nuget.org does not render. Use Markdown. ### MSKITPKG011 -`MSKITPKG011` (formerly `MSKIT_PKG011`) — the package README uses GitHub alerts (`> [!NOTE]`), which nuget.org shows as plain quotes. Use a bold lead-in such as `**Note:**`. +**Package README uses GitHub alerts** + +`MSKITPKG011` (formerly `MSKIT_PKG011`) (warning, error on CI) — the package README uses GitHub alerts (`> [!NOTE]`), which nuget.org shows as plain quotes. Use a bold lead-in such as `**Note:**`. ### MSKITPKG012 -`MSKITPKG012` (formerly `MSKIT_PKG012`) — the package README loads images from a host nuget.org blocks. Host them on an allowed domain (`img.shields.io`, `raw.githubusercontent.com`, …). +**Package README loads images from a blocked host** + +`MSKITPKG012` (formerly `MSKIT_PKG012`) (warning, error on CI) — the package README loads images from a host nuget.org blocks. Host them on an allowed domain (`img.shields.io`, `raw.githubusercontent.com`, …). ### MSKITPKG013 -`MSKITPKG013` (formerly `MSKIT_PKG013`) — the package version is not [SemVer 2.0](https://semver.org/) (`MSKit_SemVerRegex`). +**Package version is not SemVer 2.0** + +`MSKITPKG013` (formerly `MSKIT_PKG013`) (warning, error on CI) — the package version is not [SemVer 2.0](https://semver.org/) (`MSKit_SemVerRegex`). ### MSKITPKG014 -`MSKITPKG014` (formerly `MSKIT_PKG014`) — no repository or project URL. Set `RepositoryUrl`, or build from a git clone whose `origin` remote Source Link can read. +**Package has no repository or project URL** + +`MSKITPKG014` (formerly `MSKIT_PKG014`) (warning, error on CI) — no repository or project URL. Set `RepositoryUrl`, or build from a git clone whose `origin` remote Source Link can read. ### MSKITPKG015 -`MSKITPKG015` (formerly `MSKIT_PKG015`) — the icon is not a PNG or JPEG of `MSKit_PackageIconSize` × `MSKit_PackageIconSize` pixels (128); the size is checked on both formats. +**Package icon has the wrong format or size** + +`MSKITPKG015` (formerly `MSKIT_PKG015`) (warning, error on CI) — the icon is not a PNG or JPEG of `MSKit_PackageIconSize` × `MSKit_PackageIconSize` pixels (128); the size is checked on both formats. ### MSKITPKG016 -`MSKITPKG016` (formerly `MSKIT_PKG016`) — no `PackageReleaseNotes`. The kit fills them on `github.com`, and on any host the generated readme knows with `MSKit_PackageReadmeFrom`; elsewhere set them (a link to the changelog is enough). +**Package has no `PackageReleaseNotes`** + +`MSKITPKG016` (formerly `MSKIT_PKG016`) (warning, error on CI) — no `PackageReleaseNotes`. The kit fills them on `github.com`, and on any host the generated readme knows with `MSKit_PackageReadmeFrom`; elsewhere set them (a link to the changelog is enough). ### MSKITPKG017 -`MSKITPKG017` (formerly `MSKIT_PKG017`) — the package README has relative links, which break on nuget.org. Use absolute URLs, or generate the readme with `MSKit_PackageReadmeFrom`. +**Package README has relative links** + +`MSKITPKG017` (formerly `MSKIT_PKG017`) (warning, error on CI) — the package README has relative links, which break on nuget.org. Use absolute URLs, or generate the readme with `MSKit_PackageReadmeFrom`. ### MSKITPKG018 -`MSKITPKG018` (formerly `MSKIT_PKG018`) — the package has an open-source licence expression, but its `Copyright` says "all rights reserved". Use `Copyright (c) YEAR OWNER`. +**`Copyright` contradicts the open-source licence** + +`MSKITPKG018` (formerly `MSKIT_PKG018`) (warning, error on CI) — the package has an open-source licence expression, but its `Copyright` says "all rights reserved". Use `Copyright (c) YEAR OWNER`. ### MSKITPKG019 -`MSKITPKG019` (formerly `MSKIT_PKG019`) — the package README contains a Mermaid diagram, which nuget.org shows as code. Link to the diagram on the repository host instead. +**Package README contains a Mermaid diagram** + +`MSKITPKG019` (formerly `MSKIT_PKG019`) (warning, error on CI) — the package README contains a Mermaid diagram, which nuget.org shows as code. Link to the diagram on the repository host instead. ### MSKITPKG020 +**Package readme cannot be generated as asked** + `MSKITPKG020` (formerly `MSKIT_PKG020`) (warning) — the readme cannot be generated as asked: the `MSKit_PackageReadmeFrom` file is missing, a `nuget:skip` / `nuget:only` marker is unbalanced, a link leaves the repository, or links cannot be rewritten (no repository URL, an unknown host or provider, no commit). The message names the line. ### MSKITPKG021 +**Generated readme loads an image from a blocked host** + `MSKITPKG021` (formerly `MSKIT_PKG021`) (warning) — a generated readme loads an image from a host nuget.org does not render images from; the message names the image and its README line. ### MSKITPKG022 +**Repository is private or internal** + `MSKITPKG022` (formerly `MSKIT_PKG022`) (warning) — the repository is private or internal (`MSKit_RepositoryVisibility`, else GitLab's `CI_PROJECT_VISIBILITY`), so the readme's links will not open for package readers. ## Versioning @@ -112,22 +156,32 @@ Background: [Versioning](../versioning.md). ### MSKITVER001 +**`Version` is declared in the project** + `MSKITVER001` (formerly `MSKIT_VER001`) (error) — the csproj declares `` while a template strategy renders the version, so the value would be ignored. Remove it and declare `VersionPrefix` in `Directory.Version.props`, or set `MSKit_VersionStrategy=Manual`. ### MSKITVER002 +**Version template has an unknown placeholder** + `MSKITVER002` (formerly `MSKIT_VER002`) (error) — a version template uses an unknown placeholder. The message lists the valid ones ([placeholders](../versioning.md#placeholders)). ### MSKITVER004 +**`VersionTag` is empty** + `MSKITVER004` (formerly `MSKIT_VER004`) (error) — `MSKit_VersionStrategy=VersionTag` but `VersionTag` is empty. Pass `-p:VersionTag=1.2.3`, or use `ReleaseTag`. ### MSKITVER006 +**Release tag is not SemVer 2.0** + `MSKITVER006` (formerly `MSKIT_VER006`) (error, stops restore) — a tag build whose tag, after removing a leading `v`, is not [SemVer 2.0](https://semver.org/) (`MSKit_ReleaseTagRegex`). Delete the tag and its release and tag again (`v2.1.0`, `v2.1.0-beta.1`). ### MSKITVER007 +**Release tag differs from `VersionPrefix`** + `MSKITVER007` (formerly `MSKIT_VER007`) (warning) — the tag's `MAJOR.MINOR.PATCH` differs from the `VersionPrefix` the repository declares. Bump `VersionPrefix` after the release so branch builds sort above it; `MSKit_SkipAudit_ReleaseTagPrefix=True` silences it. ## References @@ -136,26 +190,38 @@ Background: [reference checks](../build.md#reference-checks), [central package v ### MSKITDUP001 +**`PackageVersion` repeats one the kit provides** + `MSKITDUP001` (formerly `MSKIT_DUP001`) (error, stops restore) — `Directory.Packages.props` declares a `PackageVersion` the kit already provides. Delete the line; to pin another version set the kit's `MSKit_PackageVersion_*` property, or turn the kit's versions off with `MSKit_ImplicitPackageVersions=False`. `MSKit_SkipAudit_ImplicitPackageDuplicates=True` skips the check. ### MSKITPRE001 +**Prerelease package on a stable branch** + `MSKITPRE001` (formerly `MSKIT_PRE001`) (warning) — a stable-branch build references a prerelease version of a package whose id starts with `MSKit_PrereleasePackagePrefix`. Use a stable version; `MSKit_PrereleasePackageCheckAsWarning=false` makes it an error. ### MSKITRES001 +**Package reference is prohibited** + `MSKITRES001` (formerly `MSKIT_RES001`) (error) — a referenced package is banned by an `MSKit_RestrictPackageReference` item with `Type="Error"`. Remove it or use the suggested alternative; `SkipGlobalRestriction="True"` on the one `PackageReference` turns it into a warning. ### MSKITRES002 +**Package reference is discouraged** + `MSKITRES002` (formerly `MSKIT_RES002`) (warning) — a referenced package is discouraged by an `MSKit_RestrictPackageReference` item with `Type="Warning"`. `SkipGlobalRestriction="True"` on the reference silences it. ### MSKITRES003 +**Project reference is not allowed** + `MSKITRES003` (formerly `MSKIT_RES003`) (error) — with `MSKit_RestrictProjectReferences=True` (or `MSKit_RestrictReferences=True`), a `ProjectReference` lacks `Allowed="True"`. ### MSKITRES004 +**Package reference is not allowed** + `MSKITRES004` (formerly `MSKIT_RES004`) (error) — with `MSKit_RestrictPackageReferences=True` (or `MSKit_RestrictReferences=True`), a `PackageReference` lacks `Allowed="True"`. ## Shared properties @@ -164,26 +230,38 @@ Background: [target frameworks declared once](../build.md#target-frameworks-decl ### MSKITSHARED006 +**`TargetFramework` repeats the shared value** + `MSKITSHARED006` (formerly `MSKIT_SHARED006`) (error) — the csproj declares the same `TargetFramework` as `Directory.Build.props`. Delete it from the csproj. ### MSKITSHARED007 +**`TargetFrameworks` repeats the shared value** + `MSKITSHARED007` (formerly `MSKIT_SHARED007`) (error) — the csproj declares the same `TargetFrameworks` as `Directory.Build.props`. Delete it from the csproj. ### MSKITSHARED008 +**`TargetFramework` overrides the shared value** + `MSKITSHARED008` (formerly `MSKIT_SHARED008`) (warning) — the csproj overrides the shared `TargetFramework` with another value. Remove it, or accept it with `MSKit_SkipAudit_TargetFrameworkOverride=True` in the csproj. ### MSKITSHARED009 +**`TargetFrameworks` overrides the shared value** + `MSKITSHARED009` (formerly `MSKIT_SHARED009`) (warning) — the csproj overrides the shared `TargetFrameworks` with another value. Remove it, or accept it with `MSKit_SkipAudit_TargetFrameworkOverride=True`. ### MSKITSHARED010 +**Both `TargetFramework` and `TargetFrameworks` are declared** + `MSKITSHARED010` (formerly `MSKIT_SHARED010`) (error) — the csproj declares both `TargetFramework` and `TargetFrameworks` (an empty `` counts). Keep one. ### MSKITSHARED020 +**`TreatWarningsAsErrors` differs from the shared value** + `MSKITSHARED020` (formerly `MSKIT_SHARED020`) (error, developer machines only) — the csproj's final `TreatWarningsAsErrors` differs from the shared value. Align it, or set `MSKit_SkipAudit_TreatWarningsAsErrors=True`. ## Project types @@ -192,18 +270,26 @@ Background: [Roslyn components](../roslyn.md). ### MSKITCORE001 +**Project name matches several project types** + `MSKITCORE001` (formerly `MSKIT_CORE001`) (error) — the project name matches more than one detection regex (analyzer, code fix, source generator). Rename the project, narrow a `MSKit_*ProjectNameRegex`, or set the matching `MSKit_Disable*AutoDetect=true` in `Directory.Build.props` above the kit import. ### MSKITROSLYN001 +**`Project.CodeAnalyzer` part is not installed** + `MSKITROSLYN001` (formerly `MSKIT_ROSLYN001`) (error) — the name matches the analyzer regex, but the `Project.CodeAnalyzer` part is not installed. `sh .toolkit/update.sh --add Project.CodeAnalyzer`, or rename the project, or set `MSKit_DisableCodeAnalyzerAutoDetect=true` in `Directory.Build.props` above the kit import. ### MSKITROSLYN002 +**`Project.CodeFixer` part is not installed** + `MSKITROSLYN002` (formerly `MSKIT_ROSLYN002`) (error) — the name matches the code-fix regex, but `Project.CodeFixer` is not installed. Add the part, rename, or set `MSKit_DisableCodeFixerAutoDetect=true` in `Directory.Build.props` above the kit import. ### MSKITROSLYN003 +**`Project.SourceGenerator` part is not installed** + `MSKITROSLYN003` (formerly `MSKIT_ROSLYN003`) (error) — the name matches the source-generator regex, but `Project.SourceGenerator` is not installed. Add the part, rename, or set `MSKit_DisableSourceGeneratorAutoDetect=true` in `Directory.Build.props` above the kit import. ## Testing @@ -212,54 +298,80 @@ Background: [Testing](../testing.md). ### MSKITTEST005 +**xUnit v3 needs net8.0 or later** + `MSKITTEST005` (formerly `MSKIT_TEST005`) (error) — an xUnit v3 test project targets a framework older than net8.0. ### MSKITTEST010 +**Test project props imported before `MSKit_TestingFramework` is set** + `MSKITTEST010` (formerly `MSKIT_TEST010`) (error) — `$(TestsProjectCommonPropsPath)` was imported before `MSKit_TestingFramework` was set. Put the `PropertyGroup` above the `Import`. ### MSKITTEST011 +**`MSKit_TestingFramework` changed after the test project props import** + `MSKITTEST011` (formerly `MSKIT_TEST011`) (error) — `MSKit_TestingFramework` changed after `$(TestsProjectCommonPropsPath)` was imported. Move the `PropertyGroup` above the `Import`. ### MSKITTEST012 +**No wiring for the testing framework** + `MSKITTEST012` (formerly `MSKIT_TEST012`) (error) — no wiring exists for the `MSKit_TestingFramework` of an explicit test project. Use `xunit.v3`, or point `MSKit_TestingFramework_CommonPropsPath` at your own props file. ### MSKITTEST013 +**`IsTestsProject` is set in the project** + `MSKITTEST013` (formerly `MSKIT_TEST013`) (error) — the csproj sets `IsTestsProject` directly, too late for the props-phase wiring. Rename the project to match `MSKit_TestsProjectNameRegex`, or use the explicit endpoint `$(TestsProjectCommonPropsPath)`. ### MSKITTEST014 +**Test project is marked as a test helper library** + `MSKITTEST014` (formerly `MSKIT_TEST014`) (error) — the name matches the test-project regex, but the project is marked as a test helper library. Rename it (`Acme.TestUtils`), narrow the regex, or drop the helper-library flag. ### MSKITTEST020 +**Test library props imported before `MSKit_TestingFramework` is set** + `MSKITTEST020` (formerly `MSKIT_TEST020`) (error) — `$(TestsLibProjectCommonPropsPath)` was imported before `MSKit_TestingFramework` was set. ### MSKITTEST021 +**`MSKit_TestingFramework` changed after the test library props import** + `MSKITTEST021` (formerly `MSKIT_TEST021`) (error) — `MSKit_TestingFramework` changed after `$(TestsLibProjectCommonPropsPath)` was imported. ### MSKITTEST022 +**No helper-library wiring for the testing framework** + `MSKITTEST022` (formerly `MSKIT_TEST022`) (error) — no helper-library wiring exists for `MSKit_TestingFramework`. Use `xunit.v3`, or set `MSKit_TestingFramework_LibCommonPropsPath`. ### MSKITTEST025 +**`MSKit_TestingFramework` is not set** + `MSKITTEST025` (formerly `MSKIT_TEST025`) (error) — a test project or helper library has no `MSKit_TestingFramework`. Set it in `Directory.Build.props` (the owner layer sets `xunit.v3`). ### MSKITTEST026 +**No installed part wires the testing framework** + `MSKITTEST026` (formerly `MSKIT_TEST026`) (error) — no installed part wires the `MSKit_TestingFramework` value. Use `xunit.v3` (part `Testing.XUnit.v3`), or set `MSKit_TestingFramework_CommonPropsPath`. ### MSKITTEST030 +**`MSKit_TestsDir` is empty** + `MSKITTEST030` (formerly `MSKIT_TEST030`) (warning) — `MSKit_TestsDir` is empty, so `InternalsVisibleTo` cannot be added. Set it, or turn `InternalsVisibleToAllTestsProjects` off. ### MSKITTEST031 +**`MSKit_TestsDir` does not exist** + `MSKITTEST031` (formerly `MSKIT_TEST031`) (warning) — `MSKit_TestsDir` points at a folder that does not exist. ## PackageAsProj @@ -268,8 +380,12 @@ Background: [PackageAsProj](../optional-parts.md#packageasproj). ### MSKITPAP001 +**Package switched to a project is still resolved from the package** + `MSKITPAP001` (formerly `MSKIT_PAP001`) (error) — a package switched to a `ProjectReference` is still resolved from the package. Run `dotnet restore --force`. ### MSKITPAP002 +**Package switched back from a project is not restored** + `MSKITPAP002` (formerly `MSKIT_PAP002`) (error) — a package switched back from a project is not restored yet. Run `dotnet restore --force`, or set `PackageAsProj_SkipChecks=True`. diff --git a/docs/reference/diagnostic-catalog.md b/docs/reference/diagnostic-catalog.md new file mode 100644 index 0000000..c91d65d --- /dev/null +++ b/docs/reference/diagnostic-catalog.md @@ -0,0 +1,50 @@ +# Diagnostic catalog + +Every code the kit reports is also declared as an MSBuild item, `BuildDiagnosticDescriptor`, so a tool that collects a build's warnings and errors can name, group and explain them without parsing this documentation. The [code reference](./codes.md) is the same catalog for readers. + +## The item + +```xml + +``` + +| Metadata | Value | +| --- | --- | +| `Include` | The code | +| `Title` | The code's short name: the bold line that opens its section of the code reference | +| `MessageFormat` | The text the build reports, with `{0}`, `{1}`, … where it inserts a value such as a project name, a path or a list; a value used twice keeps its number, and `%0A` is a line break | +| `Description` | The opening sentence or two of the code's section, as plain text | +| `Category` | The family the code's section is under: `Packaging`, `Versioning`, `References`, `Shared properties`, `Project types`, `Testing` or `PackageAsProj` | +| `DefaultSeverity` | `Warning` or `Error`: how the code is reported on a developer machine with the kit's defaults. The section says what changes it; the `MSKITPKG001`-`MSKITPKG019` checks, for one, become errors on CI | +| `HelpLink` | The code's section, the same link the warning or error carries; it follows `MSKit_CodesHelpBaseUrl` | + +`Category` is the family rather than the letters of the code because a family is what a reader groups by, and two of them hold more than one prefix (`References` has `DUP`, `PRE` and `RES`); the prefix is already in the code. + +## Where the items live + +Each part declares the codes it reports in its own folder, `.toolkit/msbuild/DragoAnt.MSBuildKit./diagnostic.descriptors.props`, which the part's `init.props` imports. A project therefore sees the descriptors of the parts it has installed and no others, and the `DefiningProjectFullPath` of an item names the part that owns the code; `.toolkit/kit.json` holds the kit version. The [code reference](./codes.md) lists which part reports which family. + +```sh +dotnet msbuild src/MyLibrary/MyLibrary.csproj -getItem:BuildDiagnosticDescriptor +``` + +prints the catalog of one project as JSON. It lists every code the installed parts can report; which of them a build did report is in that build's own output. + +A reserved code has no item: `VER003` was never reported and never will be. + +## Adding your own + +The item is not tied to the kit's codes or prefix, so your own checks can join the same catalog. + +- **Checks in a repository or in a company's shared build files:** put the items in a props file that ships next to the targets that report the codes, and import it from your props (for instance through `MSKit_AfterInitProps`, [Customizing](../customizing.md#hooks-around-the-kit)). Keep one file per set of checks that is versioned together, never one central file for all of them: a collector attributes a code to the file that defines its item. +- **A part added to a fork of the kit:** add `diagnostic.descriptors.props` to the part's folder and import it from the part's `init.props` ([CONTRIBUTING](../../CONTRIBUTING.md#changing-the-kit)). + +## How the catalog stays true + +The items and the code reference are both written by hand, and `sh tools/docs-check.sh`, which CI runs, fails when they drift. The reference cannot be generated from the items, because its sections carry what a descriptor has no place for: the fix, the switches, the links. Generating the items from the reference would mean parsing prose into XML and committing the output beside its source, which is two copies again with a generator to maintain. So the check compares them instead: every reported code has exactly one item, in the part that reports it; `Title`, `Category`, `DefaultSeverity` and `Description` equal the title line, family, `(warning)` / `(error)` mark and opening sentences of the code's section; `MessageFormat` equals the text of the `` or `` that reports the code, expressions replaced by placeholders; `HelpLink` equals the task's. diff --git a/docs/reference/properties.md b/docs/reference/properties.md index 5ccdde9..e5a3ef6 100644 --- a/docs/reference/properties.md +++ b/docs/reference/properties.md @@ -29,6 +29,7 @@ Every property and item the kit sets or reads, grouped by topic. **Set** = a val | `MSKit_ProjectObjDir` | out | | The project's `obj` folder, absolute | | `MSKit_Templates` | set | `.toolkit/.local/` | Templates for local files ([Local files](../local-files.md)) | | `MSKit_Diagnostic` | set | `false` | Reserved; nothing reads it in this version | +| `BuildDiagnosticDescriptor` | item | one per code of each installed part | A code the kit reports, with its `Title`, `MessageFormat`, `Description`, `Category`, `DefaultSeverity` and `HelpLink`, for tools that read a build's diagnostics ([diagnostic catalog](./diagnostic-catalog.md)) | | `MSKit_CodesHelpBaseUrl` | set | `https://github.com/DragoAnt/MSBuildKit/blob/main/docs/reference/codes.md` | The page every warning and error links to (`HelpLink`); the link adds `#` and the code in lower case ([code reference](./codes.md)). Point it at your own copy of the page | ## Versioning diff --git a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Core/diagnostic.descriptors.props b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Core/diagnostic.descriptors.props new file mode 100644 index 0000000..4ef8828 --- /dev/null +++ b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Core/diagnostic.descriptors.props @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Core/init.props b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Core/init.props index edfb4ee..b51e803 100644 --- a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Core/init.props +++ b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Core/init.props @@ -7,4 +7,6 @@ + + diff --git a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.PackageAsProj/diagnostic.descriptors.props b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.PackageAsProj/diagnostic.descriptors.props new file mode 100644 index 0000000..e8bf423 --- /dev/null +++ b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.PackageAsProj/diagnostic.descriptors.props @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.PackageAsProj/init.props b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.PackageAsProj/init.props new file mode 100644 index 0000000..8357582 --- /dev/null +++ b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.PackageAsProj/init.props @@ -0,0 +1,5 @@ + + + + + diff --git a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Packaging/diagnostic.descriptors.props b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Packaging/diagnostic.descriptors.props new file mode 100644 index 0000000..cb84013 --- /dev/null +++ b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Packaging/diagnostic.descriptors.props @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Packaging/init.props b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Packaging/init.props index 91ed9f6..90f6c82 100644 --- a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Packaging/init.props +++ b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Packaging/init.props @@ -24,4 +24,6 @@ auto + + diff --git a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing.XUnit.v3/diagnostic.descriptors.props b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing.XUnit.v3/diagnostic.descriptors.props new file mode 100644 index 0000000..6ff1884 --- /dev/null +++ b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing.XUnit.v3/diagnostic.descriptors.props @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing.XUnit.v3/init.props b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing.XUnit.v3/init.props index 86ddd60..3ca5546 100644 --- a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing.XUnit.v3/init.props +++ b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing.XUnit.v3/init.props @@ -9,4 +9,6 @@ + + diff --git a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing/diagnostic.descriptors.props b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing/diagnostic.descriptors.props new file mode 100644 index 0000000..8a425ef --- /dev/null +++ b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing/diagnostic.descriptors.props @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + diff --git a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing/init.props b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing/init.props index 73ea576..17d75a7 100644 --- a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing/init.props +++ b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing/init.props @@ -12,4 +12,6 @@ + + diff --git a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit/diagnostic.descriptors.props b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit/diagnostic.descriptors.props new file mode 100644 index 0000000..e71d1ad --- /dev/null +++ b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit/diagnostic.descriptors.props @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit/init.props b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit/init.props index ef44584..ad1bf33 100644 --- a/kit/.toolkit/msbuild/DragoAnt.MSBuildKit/init.props +++ b/kit/.toolkit/msbuild/DragoAnt.MSBuildKit/init.props @@ -14,4 +14,6 @@ + + diff --git a/kit/.toolkit/msbuild/init.props b/kit/.toolkit/msbuild/init.props index 580345c..bd6a614 100644 --- a/kit/.toolkit/msbuild/init.props +++ b/kit/.toolkit/msbuild/init.props @@ -20,6 +20,7 @@ + diff --git a/tests/codes.sh b/tests/codes.sh index 3f6c937..b644274 100644 --- a/tests/codes.sh +++ b/tests/codes.sh @@ -1,7 +1,7 @@ # Codes: every code is spelled MSKIT, and every diagnostic links to its section of # docs/reference/codes.md through MSKit_CodesHelpBaseUrl. The terminal logger prints the HelpLink as # an OSC 8 hyperlink on the code, which is how the link is read back here. -# Sourced by tests/run.sh: uses its pass, bad, $out, $here, $lib, $fixtures, $clean_env and $ci_env. +# Sourced by tests/run.sh: uses its pass, bad, $out, $here, $kit, $sample, $lib, $fixtures, $clean_env and $ci_env. if grep -rn 'MSKIT[_]' "$here/kit" > "$out/codes-old-spelling.log"; then bad "codes: the kit spells a code with an underscore (see $out/codes-old-spelling.log)"; head -n 5 "$out/codes-old-spelling.log" @@ -21,3 +21,52 @@ for code in mskitpkg001 mskitpkg010; do grep -qF "]8;;https://codes.example/kit.md#$code" "$out/codes-helplink-pkg.log" \ && pass "codes: MSKit_CodesHelpBaseUrl moves the $code link" || bad "codes: the $code link ignores MSKit_CodesHelpBaseUrl (see $out/codes-helplink-pkg.log)" done + +# The diagnostic catalog: every BuildDiagnosticDescriptor item reaches an evaluated project, defined +# by the file of the part that reports the code (a collector reads DefiningProjectFullPath). +catalog_items() { + proj="$1"; name="$2"; shift 2 + $clean_env dotnet msbuild "$proj" -nologo -getItem:BuildDiagnosticDescriptor "$@" 2> "$out/$name.err" | tr -d '\r' > "$out/$name.json" || true + awk ' + function val(l) { sub(/^[^:]*:[[:space:]]*"/, "", l); sub(/",?[[:space:]]*$/, "", l); gsub(/\\+/, "/", l); return l } + /^[[:space:]]*"Identity":/ { id = val($0) } + /^[[:space:]]*"Title":/ { title = val($0) } + /^[[:space:]]*"DefaultSeverity":/ { severity = val($0) } + /^[[:space:]]*"HelpLink":/ { link = val($0) } + /^[[:space:]]*"DefiningProjectFullPath":/ { part = val($0); if (!sub(/^.*\/\.toolkit\/msbuild\//, "", part) || !sub(/\/diagnostic\.descriptors\.props$/, "", part)) part = "?" } + /^[[:space:]]*}/ { if (id != "") print id "\t" part "\t" severity "\t" link "\t" (title == "" ? "-" : "titled"); id = part = severity = link = title = "" } + ' "$out/$name.json" | sort > "$out/$name.tsv" +} +sh "$here/tools/docs-check.sh" --list descriptors > "$out/catalog-declared.tsv" 2> "$out/catalog-declared.err" || true + +catalog_root="$out/catalog" +rm -rf "$catalog_root"; mkdir -p "$catalog_root/App" +printf '\n \n net8.0\n \n\n' > "$catalog_root/App/App.csproj" +sh "$kit/.toolkit/update.sh" --source "$kit" --root "$catalog_root" --add PackageAsProj > "$out/catalog-install.log" 2>&1 || { bad "catalog: update.sh --add PackageAsProj failed (see $out/catalog-install.log)"; tail -n 5 "$out/catalog-install.log"; } + +catalog_items "$catalog_root/App/App.csproj" catalog-all +cut -f1,2 "$out/catalog-declared.tsv" > "$out/catalog-all.expected" +cut -f1,2 "$out/catalog-all.tsv" > "$out/catalog-all.actual" +catalog_count=$(grep -c . "$out/catalog-all.actual" || true) +[ "$catalog_count" -gt 0 ] && cmp -s "$out/catalog-all.expected" "$out/catalog-all.actual" \ + && pass "catalog: $catalog_count descriptors reach an evaluated project, each defined by the part that reports its code" \ + || { bad "catalog: the evaluated BuildDiagnosticDescriptor items differ from the kit's (diff $out/catalog-all.expected $out/catalog-all.actual)"; diff "$out/catalog-all.expected" "$out/catalog-all.actual" | head -n 10; } +grep -q " DragoAnt.MSBuildKit.PackageAsProj$" "$out/catalog-all.actual" && pass "catalog: an optional part brings its descriptors when it is installed" || bad "catalog: no descriptor is defined by the PackageAsProj part (see $out/catalog-all.tsv)" +catalog_incomplete=$(awk -F'\t' '($3 != "Warning" && $3 != "Error") || $5 != "titled"' "$out/catalog-all.tsv" | grep -c . || true) +[ "$catalog_count" -gt 0 ] && [ "$catalog_incomplete" -eq 0 ] && pass "catalog: every evaluated descriptor has a Title and a DefaultSeverity of Warning or Error" \ + || bad "catalog: $catalog_incomplete of $catalog_count evaluated descriptor(s) lack a Title or a Warning/Error DefaultSeverity (see $out/catalog-all.tsv)" +grep -q "^MSKITVER006 DragoAnt.MSBuildKit Error $codes_url#mskitver006 " "$out/catalog-all.tsv" \ + && pass "catalog: MSKITVER006 evaluates to an Error linked to $codes_url#mskitver006" || bad "catalog: MSKITVER006 is not an Error linked to its section (see $out/catalog-all.tsv)" + +catalog_items "$lib" catalog-sample +while IFS=' ' read -r code part where; do + [ -d "$sample/.toolkit/msbuild/$part" ] && printf '%s\t%s\n' "$code" "$part" +done < "$out/catalog-declared.tsv" > "$out/catalog-sample.expected" +cut -f1,2 "$out/catalog-sample.tsv" > "$out/catalog-sample.actual" +[ -s "$out/catalog-sample.actual" ] && cmp -s "$out/catalog-sample.expected" "$out/catalog-sample.actual" && ! grep -q "PackageAsProj" "$out/catalog-sample.actual" \ + && pass "catalog: the sample sees the $(grep -c . "$out/catalog-sample.actual") descriptors of its installed parts and none of a part it lacks" \ + || bad "catalog: the sample's descriptors are not those of its installed parts (diff $out/catalog-sample.expected $out/catalog-sample.actual)" + +catalog_items "$lib" catalog-moved -p:MSKit_CodesHelpBaseUrl=https://codes.example/kit.md +grep -q "^MSKITPKG001 DragoAnt.MSBuildKit.Packaging Warning https://codes.example/kit.md#mskitpkg001 " "$out/catalog-moved.tsv" \ + && pass "catalog: MSKit_CodesHelpBaseUrl moves a descriptor's HelpLink" || bad "catalog: the MSKITPKG001 descriptor ignores MSKit_CodesHelpBaseUrl (see $out/catalog-moved.tsv)" diff --git a/tests/docs.sh b/tests/docs.sh index 2aad5d2..c1a18bb 100644 --- a/tests/docs.sh +++ b/tests/docs.sh @@ -33,3 +33,51 @@ for needle in "property MSKit_SemVerRegex" "code MSKITVER004" "write the heading where=${needle%%|*}; what=${needle#*|} grep -F "$where" "$out/docs-check-broken.log" | grep -qF "$what" && pass "docs: the check reports '$needle'" || bad "docs: the check missed '$needle' (see $out/docs-check-broken.log)" done + +# The diagnostic catalog: a copy with one descriptor missing, doubled, orphaned, in the wrong part, +# in the wrong file and unimported, and with each piece of metadata wrong, must fail on each. +dc_cat="$out/docs-check-catalog" +dc_copy "$dc_cat" +dc_parts="$dc_cat/kit/.toolkit/msbuild" +dc_item() { sed "/Include=\"$2\"/,/\/>/ $3" "$dc_parts/$1/diagnostic.descriptors.props" > "$dc_cat/item.tmp" && mv "$dc_cat/item.tmp" "$dc_parts/$1/diagnostic.descriptors.props"; } +dc_fake() { printf ' ' "$1" "$2"; } +if [ -f "$dc_parts/DragoAnt.MSBuildKit/diagnostic.descriptors.props" ]; then + dc_item DragoAnt.MSBuildKit.Testing MSKITTEST031 d + dc_item DragoAnt.MSBuildKit.Packaging MSKITPKG019 d + dc_item DragoAnt.MSBuildKit.Testing.XUnit.v3 MSKITTEST005 d + sed "s|^| \n$(dc_fake MSKITVER003 mskitver003)\n$(dc_fake MSKITVER007 mskitver007)\n$(dc_fake MSKITTEST005 mskittest005)\n \n&|" \ + "$here/kit/.toolkit/msbuild/DragoAnt.MSBuildKit/diagnostic.descriptors.props" > "$dc_parts/DragoAnt.MSBuildKit/diagnostic.descriptors.props" + sed "s|^| \n$(dc_fake MSKITVER098 mskitver098)\n \n&|" \ + "$here/kit/.toolkit/msbuild/DragoAnt.MSBuildKit/audit/audit.version.targets" > "$dc_parts/DragoAnt.MSBuildKit/audit/audit.version.targets" + grep -v 'diagnostic.descriptors.props' "$here/kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Core/init.props" > "$dc_parts/DragoAnt.MSBuildKit.Core/init.props" + grep -v 'DragoAnt.MSBuildKit.PackageAsProj/init.props' "$here/kit/.toolkit/msbuild/init.props" > "$dc_parts/init.props" + dc_item DragoAnt.MSBuildKit.Core MSKITCORE001 's/#mskitcore001"/#mskitroslyn001"/' + dc_item DragoAnt.MSBuildKit.Core MSKITROSLYN001 's/DefaultSeverity="Error"/DefaultSeverity="Warning"/' + dc_item DragoAnt.MSBuildKit.Core MSKITROSLYN002 's/DefaultSeverity="Error"/DefaultSeverity="Info"/' + dc_item DragoAnt.MSBuildKit MSKITPRE001 's/DefaultSeverity="Warning"/DefaultSeverity="Error"/' + dc_item DragoAnt.MSBuildKit MSKITRES001 's/Category="[^"]*"/Category="Versioning"/' + dc_item DragoAnt.MSBuildKit MSKITDUP001 's/Description="/Description="In short: /' + dc_item DragoAnt.MSBuildKit MSKITVER004 's/MessageFormat="/MessageFormat="Oops. /' + dc_item DragoAnt.MSBuildKit.Packaging MSKITPKG004 's/MessageFormat="/MessageFormat="Oops. /' + dc_item DragoAnt.MSBuildKit.Packaging MSKITPKG003 's/ Title="[^"]*"/ Title=""/' + dc_item DragoAnt.MSBuildKit.PackageAsProj MSKITPAP002 's/Title="/Title="Not /' + dc_item DragoAnt.MSBuildKit.Testing MSKITTEST010 's/%24(/$(/' +fi +sh "$dc_cat/tools/docs-check.sh" > "$out/docs-check-catalog.log" 2>&1 && bad "docs: the check passed a broken catalog (see $out/docs-check-catalog.log)" +for needle in "code MSKITTEST031 (|has no BuildDiagnosticDescriptor item; add one to kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Testing/diagnostic.descriptors.props" \ + "code MSKITPKG019 (|has no BuildDiagnosticDescriptor item" "MSKITVER007 already has a BuildDiagnosticDescriptor" \ + "MSKITVER003 has a BuildDiagnosticDescriptor, but no or reports it" \ + "MSKITTEST005 is described in part DragoAnt.MSBuildKit, but reported by DragoAnt.MSBuildKit.Testing.XUnit.v3" \ + "audit.version.targets:|declare MSKITVER098 in its part folder, in diagnostic.descriptors.props" \ + "diagnostic.descriptors.props is not imported by kit/.toolkit/msbuild/DragoAnt.MSBuildKit.Core/init.props" \ + "does not import DragoAnt.MSBuildKit.PackageAsProj/init.props" \ + "MSKITCORE001 has HelpLink=" "MSKITROSLYN001 has DefaultSeverity=\"Warning\", but no reports it" \ + "MSKITROSLYN002 has DefaultSeverity=\"Info\"; use Warning or Error" \ + "MSKITPRE001 has DefaultSeverity=\"Error\", but its section in docs/reference/codes.md says (warning)" \ + "MSKITRES001 has Category=\"Versioning\", but its section in docs/reference/codes.md is under \"References\"" \ + "the Description of MSKITDUP001 is not the opening sentence(s)" "the MessageFormat of MSKITVER004 is not the text its task reports" \ + "the MessageFormat of MSKITPKG004 is not the text its task reports" "MSKITPKG003 has no Title" "MSKITPAP002 has Title=\"Not " \ + "MSKITTEST010 has metadata MSBuild would expand"; do + where=${needle%%|*}; what=${needle#*|} + grep -F "$where" "$out/docs-check-catalog.log" | grep -qF "$what" && pass "docs: the check reports '$needle'" || bad "docs: the check missed '$needle' (see $out/docs-check-catalog.log)" +done diff --git a/tools/docs-check.sh b/tools/docs-check.sh index 77b584e..28aa31e 100644 --- a/tools/docs-check.sh +++ b/tools/docs-check.sh @@ -1,9 +1,11 @@ #!/bin/sh # Keeps the documentation honest against the kit: every property and item the kit defines has a row # in docs/reference/properties.md, every code it reports a section headed by the code id in -# docs/reference/codes.md, every / a HelpLink to that section, every MSKit_ name and -# code the docs mention exists in the kit, no file spells a code the old way, and every link resolves. -# Usage: sh tools/docs-check.sh [--root DIR] [--list properties|items|codes|diagnostics] +# docs/reference/codes.md, every / a HelpLink to that section, every code exactly one +# BuildDiagnosticDescriptor item in the part that reports it, with the title, description, family +# and severity of its section and the text of its task, every MSKit_ name and code the docs mention +# exists in the kit, no file spells a code the old way, and every link resolves. +# Usage: sh tools/docs-check.sh [--root DIR] [--list properties|items|codes|diagnostics|descriptors] set -eu root=$(cd "$(dirname "$0")/.." && pwd) @@ -12,11 +14,11 @@ while [ $# -gt 0 ]; do case "$1" in --root) [ $# -ge 2 ] || { echo "docs-check: --root needs a value" >&2; exit 2; }; root=$(cd "$2" && pwd); shift 2 ;; --list) [ $# -ge 2 ] || { echo "docs-check: --list needs a value" >&2; exit 2; }; list="$2"; shift 2 ;; - -h|--help) sed -n '2,6p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + -h|--help) sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "docs-check: unknown argument '$1'" >&2; exit 2 ;; esac done -case "$list" in ""|properties|items|codes|diagnostics) ;; *) echo "docs-check: --list takes properties, items, codes or diagnostics" >&2; exit 2 ;; esac +case "$list" in ""|properties|items|codes|diagnostics|descriptors) ;; *) echo "docs-check: --list takes properties, items, codes, diagnostics or descriptors" >&2; exit 2 ;; esac [ -d "$root/kit/.toolkit/msbuild" ] || { echo "docs-check: no kit at $root/kit/.toolkit/msbuild" >&2; exit 2; } work=$(mktemp -d) @@ -26,6 +28,11 @@ cd "$root" # Kit inventory with XML comments removed, one process for every file: # "P " property set, "I" item, "R" property read, "C" diagnostic code, # "W " a or task ("-" for a missing attribute). +# Tab-separated, for the diagnostic catalog: "T " the same task, +# "B <MessageFormat> <Description> <Category> <DefaultSeverity> <HelpLink>" a +# BuildDiagnosticDescriptor item, "F <where> <item> <code> <metadata> <value>" an item named after a +# code (what a task with a computed Code reports), "X <where> <Project>" an import and +# "Q <file> <name> <value>" a one-line private property (a task text kept in one). find kit/.toolkit/msbuild -type f \( -name '*.props' -o -name '*.targets' -o -name '*.cs.txt' \) -exec awk ' FNR == 1 { incomment = 0; pg = 0; ig = 0; inel = 0 } { sub(/\r$/, "") } @@ -48,17 +55,49 @@ find kit/.toolkit/msbuild -type f \( -name '*.props' -o -name '*.targets' -o -na else if (tag == "ItemGroup") ig++ else if (tag == "/ItemGroup") ig-- else if (substr(tag, 1, 1) != "/" && pg > 0) print "P", tag, FILENAME ":" FNR - else if (substr(tag, 1, 1) != "/" && ig > 0 && tag ~ /^MSKit_/) print "I", tag, FILENAME ":" FNR + else if (substr(tag, 1, 1) != "/" && ig > 0 && (tag ~ /^MSKit_/ || tag == "BuildDiagnosticDescriptor")) print "I", tag, FILENAME ":" FNR } rest = out while (match(rest, /\$\(MSKit_[A-Za-z0-9_]+/)) { print "R", substr(rest, RSTART + 2, RLENGTH - 2), FILENAME ":" FNR; rest = substr(rest, RSTART + RLENGTH) } rest = out - while (match(rest, /MSKIT[A-Z]+[0-9][0-9][0-9]/)) { print "C", substr(rest, RSTART, RLENGTH), FILENAME ":" FNR; rest = substr(rest, RSTART + RLENGTH) } - if (!inel && (match(out, /<(Warning|Error)[[:space:]\/>]/) || match(out, /<(Warning|Error)$/))) { inel = 1; el = substr(out, RSTART); elat = FILENAME ":" FNR } - else if (inel) el = el " " out - if (inel && (e = closed(el))) { el = substr(el, 1, e); print "W", elat, attr(el, "Code"), attr(el, "HelpLink"); inel = 0 } + while (FILENAME !~ /diagnostic\.descriptors\.props$/ && match(rest, /MSKIT[A-Z]+[0-9][0-9][0-9]/)) { print "C", substr(rest, RSTART, RLENGTH), FILENAME ":" FNR; rest = substr(rest, RSTART + RLENGTH) } + if (match(out, /<_MSKit_[A-Za-z0-9_]+>[^<]+<\/_MSKit_[A-Za-z0-9_]+>/)) { + q = substr(out, RSTART + 1, RLENGTH - 1); name = substr(q, 1, index(q, ">") - 1); q = substr(q, index(q, ">") + 1); sub(/<\/.*$/, "", q) + print "Q " FILENAME " " name " " q + } + rest = out + if (inel) { el = el " " rest; rest = "" } + while (1) { + if (!inel) { if (!match(rest, /<[A-Za-z_][A-Za-z0-9_.]*/)) break; el = substr(rest, RSTART); rest = ""; elat = FILENAME ":" FNR; inel = 1 } + e = closed(el); if (!e) break + rest = substr(el, e + 1); el = substr(el, 1, e); inel = 0; element(el, elat) + } + } + function element(el, at, tag, code, a, name) { + match(el, /^<[A-Za-z_][A-Za-z0-9_.]*/); tag = substr(el, 2, RLENGTH - 1); code = attr(el, "Include") + if (tag == "Warning" || tag == "Error") { + print "W", at, attr(el, "Code"), attr(el, "HelpLink") + print "T " at " " tag " " attr(el, "Code") " " attr(el, "Text") + } + else if (tag == "BuildDiagnosticDescriptor") + print "B " at " " code " " attr(el, "Title") " " attr(el, "MessageFormat") " " attr(el, "Description") " " attr(el, "Category") " " attr(el, "DefaultSeverity") " " attr(el, "HelpLink") + else if (tag == "Import") print "X " at " " attr(el, "Project") + else if (code ~ /^MSKIT[A-Z]+[0-9][0-9][0-9]$/) + while (match(el, /[[:space:]][A-Za-z_][A-Za-z0-9_]*="[^"]*"/)) { + a = substr(el, RSTART + 1, RLENGTH - 2); el = substr(el, RSTART + RLENGTH); name = substr(a, 1, index(a, "=") - 1) + print "F " at " " tag " " code " " name " " substr(a, index(a, "=") + 2) + } + } + function closed(s, at, q, e) { + at = 0 + while (1) { + q = index(s, "\""); e = index(s, ">") + if (!e) return 0 + if (!q || e < q) return at + e + at += q; s = substr(s, q + 1); q = index(s, "\""); if (!q) return 0 + at += q; s = substr(s, q + 1) + } } - function closed(s, i, c, q) { for (i = 1; i <= length(s); i++) { c = substr(s, i, 1); if (c == "\"") q = !q; else if (c == ">" && !q) return i }; return 0 } function attr(s, name, v) { if (!match(s, "[[:space:]]" name "=\"[^\"]*\"")) return "-" v = substr(s, RSTART, RLENGTH); sub(/^[[:space:]]*[A-Za-z]+="/, "", v); sub(/"$/, "", v) @@ -74,6 +113,7 @@ awk -v dir="$work" ' la = a; sub(/^.*:/, "", la); lb = b; sub(/^.*:/, "", lb) return fa < fb || (fa == fb && la + 0 < lb + 0) } + /^[TBFXQ]\t/ { print > (dir "/elements"); next } $1 == "W" { print $2 "\t" $3 "\t" $4 > (dir "/diagnostics"); next } $1 == "C" { if (better($3, c[$2])) c[$2] = $3; next } $1 == "I" { if (better($3, i[$2])) i[$2] = $3; next } @@ -85,7 +125,9 @@ awk -v dir="$work" ' for (n in set) print n "\t" set[n] "\t" (n in read ? "set, read" : "set") > (dir "/properties") for (n in read) if (!(n in set)) print n "\t" read[n] "\tread" > (dir "/properties") }' "$work/raw" -for k in properties items codes diagnostics; do touch "$work/$k"; sort -o "$work/$k" "$work/$k"; done +touch "$work/elements" +awk -F'\t' '$1 == "B" { part = $2; sub(/^kit\/\.toolkit\/msbuild\//, "", part); if (!sub(/\/.*$/, "", part)) part = "-"; print $3 "\t" part "\t" $2 }' "$work/elements" > "$work/descriptors" +for k in properties items codes diagnostics descriptors; do touch "$work/$k"; sort -o "$work/$k" "$work/$k"; done if [ -n "$list" ]; then cat "$work/$list"; exit 0; fi @@ -120,9 +162,17 @@ awk -v dir="$work" ' split($0, cell, "|"); c = cell[2] while (match(c, /`[^`]+`/)) { print "D\t" f "\t" substr(c, RSTART + 1, RLENGTH - 2); c = substr(c, RSTART + RLENGTH) } } + f == "docs/reference/codes.md" && /^##[[:space:]]/ { family = $0; sub(/^##[[:space:]]+/, "", family); section = "" } f == "docs/reference/codes.md" && /^#+[[:space:]]+`?MSKIT[_]?[A-Z]+[0-9][0-9][0-9]/ { match($0, /MSKIT[_]?[A-Z]+[0-9][0-9][0-9]/); c = substr($0, RSTART, RLENGTH) print (c ~ /^MSKIT[_]/ ? "U\t" f ":" FNR "\t" c : "D\t" f "\t" c) + section = c; print "K\t" c "\tfamily\t" family + } + f == "docs/reference/codes.md" && section != "" && /^\*\*.*\*\*$/ && !((section, "title") in sectionhas) { + sectionhas[section, "title"] = 1; t = substr($0, 3, length($0) - 4); gsub(/`/, "", t); print "K\t" section "\ttitle\t" t + } + f == "docs/reference/codes.md" && section != "" && index($0, "`" section "`") == 1 && !((section, "lead") in sectionhas) { + sectionhas[section, "lead"] = 1; print "K\t" section "\tlead\t" $0 } { line = $0; gsub(/`[^`]*`/, "", line) while (match(line, /\]\([^) ]+\)/)) { print "L\t" f "\t" FNR "\t" d "\t" substr(line, RSTART + 2, RLENGTH - 3); line = substr(line, RSTART + RLENGTH) } } @@ -145,6 +195,43 @@ awk -v dir="$work" -F'\t' ' return out } function problem(m) { print "docs-check: " m; problems++ } + function fileof(w) { sub(/:[0-9]+$/, "", w); return w } + function partof(w) { sub(/^kit\/\.toolkit\/msbuild\//, "", w); return sub(/\/.*$/, "", w) ? w : "" } + function unxml(t) { gsub(/</, "<", t); gsub(/>/, ">", t); gsub(/"/, "\"", t); gsub(/'/, "\047", t); gsub(/&/, "\\&", t); return t } + function squeeze(t) { gsub(/[[:space:]]+/, " ", t); sub(/^ /, "", t); sub(/ $/, "", t); return t } + # A task text with every MSBuild expression as {}, and a message format with every {n} as {}. + function skeleton(t, res, ch, depth) { + res = "" + while (match(t, /[$@%]\(/)) { + res = res substr(t, 1, RSTART - 1) "{}"; t = substr(t, RSTART + 1); depth = 0 + while (match(t, /[()]/)) { ch = substr(t, RSTART, 1); t = substr(t, RSTART + 1); if (ch == "(") depth++; else if (--depth == 0) break } + if (depth) t = "" + } + return squeeze(unxml(res t)) + } + function formatskeleton(t) { gsub(/[{][0-9]+[}]/, "\001", t); gsub(/[{][{]/, "{", t); gsub(/[}][}]/, "}", t); gsub(/\001/, "{}", t); return squeeze(unxml(t)) } + # Markdown as the plain text a descriptor carries: no code ticks, no bold, a link as its text. + function plain(t, label) { + gsub(/`/, "", t); gsub(/\*\*/, "", t) + while (match(t, /\[[^]]*\]\([^)]*\)/)) { label = substr(t, RSTART + 1, RLENGTH - 1); label = substr(label, 1, index(label, "](") - 1); t = substr(t, 1, RSTART - 1) label substr(t, RSTART + RLENGTH) } + return squeeze(t) + } + function unescape(t) { gsub(/%24/, "$", t); gsub(/%40/, "@", t); gsub(/%3[Bb]/, ";", t); gsub(/%25/, "%", t); return t } + function reports(code, w, kind, text, f) { + f = fileof(w) + if (!(code in rpart)) { rcode[++nr] = code; rwhere[code] = w } + rpart[code] = rpart[code] "|" partof(f) "|"; rkind[code] = rkind[code] "|" kind "|" + if (text ~ /^\$\(_MSKit_[A-Za-z0-9_]+\)$/ && ((f, substr(text, 3, length(text) - 3)) in private)) text = private[f, substr(text, 3, length(text) - 3)] + rtext[code, ++rtexts[code]] = skeleton(text) + } + FILENAME == dir "/elements" { + if ($1 == "T") { twhere[++nt] = $2; tkind[nt] = $3; tcode[nt] = $4; ttext[nt] = $5 } + else if ($1 == "B") { bwhere[++nb] = $2; bcode[nb] = $3; btitle[nb] = $4; bformat[nb] = $5; bdescription[nb] = $6; bcategory[nb] = $7; bseverity[nb] = $8; blink[nb] = $9 } + else if ($1 == "F") { finding[fileof($2), $3, $4, $5] = $6; if (!((fileof($2), $3, $4) in findingseen)) { findingseen[fileof($2), $3, $4] = 1; findingcodes[fileof($2), $3] = findingcodes[fileof($2), $3] " " $4 } } + else if ($1 == "X") imports[fileof($2)] = imports[fileof($2)] "|" $3 "|" + else if ($1 == "Q") private[$2, $3] = $4 + next + } FILENAME == dir "/properties" || FILENAME == dir "/items" { known[$1] = $2; what[$1] = (FILENAME == dir "/items" ? "item" : "property"); order[++n] = $1; next } FILENAME == dir "/codes" { code[$1] = $2; corder[++nc] = $1; next } FILENAME == dir "/diagnostics" { dwhere[++nd] = $1; dcode[nd] = $2; dlink[nd] = $3; next } @@ -157,6 +244,7 @@ awk -v dir="$work" -F'\t' ' $1 == "U" { problem($2 ": write the heading as " gensub_id($3) " so its anchor is the code id without the underscore"); next } function gensub_id(c) { sub(/_/, "", c); return c } $1 == "L" { links[++nl] = $0; next } + $1 == "K" { doc[$2, $3] = $4; next } END { for (i = 1; i <= n; i++) if (!(order[i] in documented)) problem(what[order[i]] " " order[i] " (" known[order[i]] ") has no row in docs/reference/properties.md") for (i = 1; i <= nc; i++) if (!(corder[i] in documentedCode)) problem("code " corder[i] " (" code[corder[i]] ") has no section in docs/reference/codes.md") @@ -170,6 +258,61 @@ awk -v dir="$work" -F'\t' ' else if (h != want) problem(dwhere[i] ": " c " has HelpLink=\"" h "\"; expected \"" want "\"") if (anchor != "" && !(("docs/reference/codes.md#" anchor) in slug)) problem(dwhere[i] ": HelpLink anchor #" anchor " has no heading in docs/reference/codes.md") } + # The diagnostic catalog: one BuildDiagnosticDescriptor per code, in the part that reports it. + for (i = 1; i <= nt; i++) { + c = tcode[i]; src = fileof(twhere[i]) + if (c ~ /^MSKIT[A-Z]+[0-9][0-9][0-9]$/) reports(c, twhere[i], tkind[i], ttext[i]) + else if (c ~ /^%\([A-Za-z_][A-Za-z0-9_]*\.Identity\)$/) { + item = substr(c, 3, length(c) - 12) + if (!((src, item) in findingcodes)) { problem(twhere[i] ": cannot tell which codes " c " reports; declare each as <" item " Include=\"MSKIT...\"> in the same file"); continue } + nf = split(findingcodes[src, item], fc, " ") + for (k = 1; k <= nf; k++) { + text = ttext[i] + while (match(text, "%\\(" item "\\.[A-Za-z_][A-Za-z0-9_]*\\)")) { + meta = substr(text, RSTART + length(item) + 3, RLENGTH - length(item) - 4) + text = substr(text, 1, RSTART - 1) (meta == "Identity" ? fc[k] : finding[src, item, fc[k], meta]) substr(text, RSTART + RLENGTH) + } + reports(fc[k], twhere[i], tkind[i], text) + } + } + } + for (i = 1; i <= nb; i++) { + c = bcode[i]; w = bwhere[i]; src = fileof(w); p = partof(src) + if (c !~ /^MSKIT[A-Z]+[0-9][0-9][0-9]$/) { problem(w ": a BuildDiagnosticDescriptor needs Include=\"MSKIT<FAMILY><nnn>\", not \"" c "\""); continue } + if (src !~ /\/diagnostic\.descriptors\.props$/ || p == "") problem(w ": declare " c " in its part folder, in diagnostic.descriptors.props") + if (c in described) { problem(w ": " c " already has a BuildDiagnosticDescriptor at " described[c]); continue } + described[c] = w; descriptorpart[p] = src + if (!(c in rpart)) { problem(w ": " c " has a BuildDiagnosticDescriptor, but no <Warning> or <Error> reports it"); continue } + if (!index(rpart[c], "|" p "|")) { owner = rpart[c]; gsub(/\|\|/, ", ", owner); gsub(/\|/, "", owner); problem(w ": " c " is described in part " p ", but reported by " owner "; move the item there") } + if (bseverity[i] != "Warning" && bseverity[i] != "Error") problem(w ": " c " has DefaultSeverity=\"" bseverity[i] "\"; use Warning or Error") + else if (!index(rkind[c], "|" bseverity[i] "|")) problem(w ": " c " has DefaultSeverity=\"" bseverity[i] "\", but no <" bseverity[i] "> reports it") + lead = doc[c, "lead"] + if (!match(lead, /\) \((warning|error)[,)]/)) problem("docs/reference/codes.md: the " c " section does not open with `" c "` (formerly ...) (warning) or (error)") + else if (tolower(bseverity[i]) != substr(lead, RSTART + 3, RLENGTH - 4)) problem(w ": " c " has DefaultSeverity=\"" bseverity[i] "\", but its section in docs/reference/codes.md says (" substr(lead, RSTART + 3, RLENGTH - 4) ")") + if (blink[i] != "$(MSKit_CodesHelpBaseUrl)#" tolower(c)) problem(w ": " c " has HelpLink=\"" blink[i] "\"; expected \"$(MSKit_CodesHelpBaseUrl)#" tolower(c) "\", as its task sets") + title = unescape(unxml(btitle[i])) + if (btitle[i] == "-") problem(w ": " c " has no Title") + else if (!((c, "title") in doc)) problem("docs/reference/codes.md: the " c " section has no **title** line; the descriptor says \"" title "\"") + else if (title != doc[c, "title"]) problem(w ": " c " has Title=\"" title "\", but its section in docs/reference/codes.md is titled \"" doc[c, "title"] "\"") + if (bcategory[i] != doc[c, "family"]) problem(w ": " c " has Category=\"" bcategory[i] "\", but its section in docs/reference/codes.md is under \"" doc[c, "family"] "\"") + body = (index(lead, " — ") ? plain(substr(lead, index(lead, " — ") + length(" — "))) : ""); d = squeeze(unescape(unxml(bdescription[i]))) + if (bdescription[i] == "-") problem(w ": " c " has no Description") + else if (tolower(substr(d, 1, 1)) != tolower(substr(body, 1, 1)) || substr(d, 2) != substr(body, 2, length(d) - 1) \ + || (length(d) < length(body) && (d !~ /\.$/ || substr(body, length(d) + 1, 1) != " "))) + problem(w ": the Description of " c " is not the opening sentence(s) of its section in docs/reference/codes.md") + ok = 0; for (k = 1; k <= rtexts[c]; k++) if (formatskeleton(bformat[i]) == rtext[c, k]) ok = 1 + if (bformat[i] == "-") problem(w ": " c " has no MessageFormat") + else if (!ok) problem(w ": the MessageFormat of " c " is not the text its task reports, with {0}, {1}, ... for the runtime values; expected the shape \"" rtext[c, 1] "\"") + if ((btitle[i] bformat[i] bdescription[i] bcategory[i]) ~ /[$@%]\(/) problem(w ": " c " has metadata MSBuild would expand; write $( as %24(, @( as %40( and %( as %25(") + } + for (i = 1; i <= nr; i++) if (!(rcode[i] in described)) { + p = partof(fileof(rwhere[rcode[i]])) + problem("code " rcode[i] " (" rwhere[rcode[i]] ") has no BuildDiagnosticDescriptor item; add one to kit/.toolkit/msbuild/" p "/diagnostic.descriptors.props") + } + for (p in descriptorpart) { + if (!index(imports["kit/.toolkit/msbuild/" p "/init.props"], "diagnostic.descriptors.props|")) problem(descriptorpart[p] " is not imported by kit/.toolkit/msbuild/" p "/init.props") + if (!index(imports["kit/.toolkit/msbuild/init.props"], ")" p "/init.props|")) problem("kit/.toolkit/msbuild/init.props does not import " p "/init.props, so the descriptors of " p " never load") + } relative = 0 for (i = 1; i <= nl; i++) { split(links[i], l, "\t"); src = l[2]; ln = l[3]; base = l[4]; target = l[5] @@ -186,5 +329,5 @@ awk -v dir="$work" -F'\t' ' if (anchor != "" && dest ~ /\.md$/ && !((dest "#" anchor) in slug)) problem(src ":" ln ": link " target " names a heading that does not exist") } if (problems) { print "docs-check: " problems " problem(s)"; exit 1 } - printf "docs-check: %d properties and items, %d codes documented, %d diagnostics with a HelpLink; %d relative links resolve\n", n, nc, nd, relative - }' "$work/properties" "$work/items" "$work/codes" "$work/diagnostics" "$work/oldspelling" "$work/paths" "$work/docs.tsv" + printf "docs-check: %d properties and items, %d codes documented, %d diagnostics with a HelpLink, %d described in the catalog; %d relative links resolve\n", n, nc, nd, nb, relative + }' "$work/properties" "$work/items" "$work/codes" "$work/diagnostics" "$work/elements" "$work/oldspelling" "$work/paths" "$work/docs.tsv"