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
7 changes: 4 additions & 3 deletions .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
{
"gitignore": true,

// Globs of files to ignore
"ignores": ["CLAUDE.md", ".claude/**/*.md", "**/AnalyzerReleases.*.md"],

"config":{
// Default state for all rules
"default": true,

// Path to configuration file to extend
"extends": null,

// Globs of files to ignore
"ignores": ["CLAUDE.md", ".claude/**/*.md"],

// MD001/heading-increment : Heading levels should only increment by one level at a time : https://github.com/DavidAnson/markdownlint/blob/main/doc/md001.md
"heading-increment": true,

Expand Down
1 change: 1 addition & 0 deletions Buildvana.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
<Project Path="tests/Buildvana.Core.Configuration.Tests/Buildvana.Core.Configuration.Tests.csproj" />
<Project Path="tests/Buildvana.Core.JsonSchema.Tests/Buildvana.Core.JsonSchema.Tests.csproj" />
<Project Path="tests/Buildvana.Core.Versioning.Tests/Buildvana.Core.Versioning.Tests.csproj" />
<Project Path="tests/Buildvana.Sdk.SourceGenerators.Tests/Buildvana.Sdk.SourceGenerators.Tests.csproj" />
<Project Path="tests/Buildvana.Sdk.Tasks.Tests/Buildvana.Sdk.Tasks.Tests.csproj" />
<Project Path="tests/Buildvana.Tool.Tests/Buildvana.Tool.Tests.csproj" />
</Folder>
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- NuGet push feeds (`nuget.feeds`): a `release` channel and an optional `prerelease` channel, each `{ source, apiKeyEnv }`. `bv release` pushes prerelease versions to the `prerelease` feed — falling back to the `release` feed when `prerelease` is omitted — and stable versions to the `release` feed. The feed URL comes from `source`; the API key is read from the environment variable named by `apiKeyEnv`. Feed selection no longer depends on whether the repository is private: the old `private` channel is gone, although `bv` can still query a repository's visibility.
- the GitHub token (`github.tokenEnv`, default `GITHUB_TOKEN`): names the environment variable that holds the token used for release operations.
- `buildvana.json` accepts a `git.identity` (`{ name, email }`) section describing the author/committer for automated commits. It is validated and exposed, but not yet wired to release commits.
- The `ThisAssemblyClass` SDK module, removed along with code generation tasks after v1.0.0-alpha.20, has been reintroduced. Setting the `GenerateThisAssemblyClass` property to `true` (default: `false`) in a C# project generates a `ThisAssembly` static class containing constants defined via `ThisAssemblyConstant` items, using the syntax documented in [docs/ConstantsSyntax.md](docs/ConstantsSyntax.md). A set of default constants (assembly version, company, product, etc.) is defined unless the `EnableDefaultThisAssemblyConstants` property is set to `false`; the class name and namespace can be customized via the `ThisAssemblyClassName` and `ThisAssemblyClassNamespace` properties. Unlike its previous incarnation, the feature is implemented as a Roslyn incremental source generator, and supports C# projects only: setting `GenerateThisAssemblyClass` to `true` in a project in any other language raises warning BVSDK2300.

### Changes to existing features

Expand Down
72 changes: 24 additions & 48 deletions docs/ConstantsSyntax.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Syntax of constants in ThisAssembly classes and LiteralAssemblyInfo parameters
# Syntax of constants in ThisAssembly classes

<!-- markdownlint-disable MD036 -->
**Table of contents**
<!-- markdownlint-enable MD036 -->

- [Overview](#overview)
- [How Buildvana SDK parses constant and parameter values](#how-buildvana-sdk-parses-constant-and-parameter-values)
- [How Buildvana SDK parses constant values](#how-buildvana-sdk-parses-constant-values)
- [Allowed types](#allowed-types)

## Overview

Constants in ThisAssembly classes are specified via `ThisAssemblyConstant` items:
Constants in `ThisAssembly` classes are specified via `ThisAssemblyConstant` items:

```XML
<!-- Generation of a ThisAssembly class is disabled by default. -->
Expand All @@ -24,67 +24,43 @@ Constants in ThisAssembly classes are specified via `ThisAssemblyConstant` items
</ItemGroup>
```

Parameters for literal assembly attributes are specified as follows:

```XML
<!-- [assembly:SomeNamespace.NiceAttribute(2, true, null, answer: "yes", foo: @"bar+""baz""")] -->
<ItemGroup>
<LiteralAssemblyAttribute Include="SomeNamespace.Nice">
<_Parameter1>2</_Parameter1>
<_Parameter2>true</_Parameter2>
<_Parameter3 />
<answer>yes</answer>
<foo>bar+"baz"</foo>
</LiteralAssemblyAttribute>
</ItemGroup>
```

The type of a constant or parameter may also be explicitly specified:
The type of a constant may also be explicitly specified:

```XML
<ItemGroup>
<ThisAssemblyConstant Include="Answer" Value="int:42" />
</ItemGroup>

<ItemGroup>
<LiteralAssemblyAttribute Include="SomeNamespace.Nice">
<_Parameter1>int:2</_Parameter1>
<_Parameter2>bool:true</_Parameter2>
<_Parameter3 />
<answer>string:yes</answer>
<foo>"bar+"""baz"""</foo>
</LiteralAssemblyAttribute>
</ItemGroup>
```

## How Buildvana SDK parses constant and parameter values
> **NOTE:** `ThisAssembly` class generation is only supported in C# projects.

## How Buildvana SDK parses constant values

Given the `Value` metadata of a `ThisAssemblyConstant` item, or a metadata of a `LiteralAssemblyAttribute` item, Buildvana SDK performs the following steps:
Given the `Value` metadata of a `ThisAssemblyConstant` item, Buildvana SDK performs the following steps:

- If the metadata is empty, the resulting object is `null` (`Nothing` in VB).
**Examples:** `<_Parameter1 />` -> `null`; `<_Parameter1></_Parameter1>` -> `null`.
- If the metadata is empty, the resulting constant is a null string (`public const string? Name = null;`).
- If the first and last characters of the metadata are double quotes, the result is a `System.String` whose value is the string between the double quotes. In this case, _double quote characters within the metadata must be doubled._
**Examples:** `<_Parameter1>""</_Parameter1>` -> `""` (the empty string); `<_Parameter1>"""Murder"", she wrote"</_Parameter1>` -> `"\"Murder\", she wrote"`.
**Examples:** `""` -> the empty string; `"""Murder"", she wrote"` -> `"Murder", she wrote`.
- If the metadata contains a colon, it is assumed to be of the form `type:value`, where `type` must be one of the strings listed in the table [below](#allowed-types), and `value` must be parsable as the specified type. If `type` is not recognized, or `value` cannot be successfully parsed, an error is logged and the build stops.
**Examples:** `<_Parameter1>int:42</_Parameter1>` -> `42`; `<_Parameter1>long:42</_Parameter1>` -> `42L` (`42&` in VB).
**Examples:** `int:42` -> `42`; `long:42` -> `42L`.
- If the metadata can be successfully parsed as a `System.Int32`, the result is the parsed value.
**Examples:** `<_Parameter1>42</_Parameter1>` -> `42`; `<_Parameter1>-13</_Parameter1>` -> `-13`.
**Examples:** `42` -> `42`; `-13` -> `-13`.
- If the metadata can be successfully parsed as a `System.Int64`, the result is the parsed value.
**Examples:** `<_Parameter1>12345678901234567890</_Parameter1>` -> `12345678901234567890L` (`12345678901234567890&` in VB); `<_Parameter1>-99998888777766665555</_Parameter1>` -> `-99998888777766665555L` (`-99998888777766665555&` in VB).
**Examples:** `9999999999` -> `9999999999L`; `-9999999999` -> `-9999999999L`.
- If the metadata can be successfully parsed as a `System.Boolean`, the result is the parsed value.
**Examples:** `<_Parameter1>true</_Parameter1>` -> `true`; `<_Parameter1>false</_Parameter1>` -> `false`.
**Examples:** `true` -> `true`; `false` -> `false`.
- If none of the previous steps yields a result, the result is a `System.String` whose value is the metadata, unchanged.
**Examples:** `<_Parameter1>foo</_Parameter1>` -> `"foo"`; `<_Parameter1>false90</_Parameter1>` -> `"false90"`.
**Examples:** `foo` -> `"foo"`; `false90` -> `"false90"`.

## Allowed types

The following table lists the recognized types for constants and parameters, along with
The following table lists the recognized types for constants, along with the prefixes that select each of them in the `type:value` syntax.

| Type | Recognized prefixes (case-insensitive) |
| -------------- | ----------------------------------------- |
| System.UInt8 | `System.Byte`, `byte`, `uint8` |
| System.Int16 | `System.Int16`, `short`, `int16` |
| System.Int32 | `System.Int32`, `int`, `int32`, `Integer` |
| System.Int64 | `System.Int64`, `long`, `int64` |
| System.Boolean | `System.Boolean`, `bool`, `Boolean` |
| System.String | `System.String`, `string` |
| Type | Recognized prefixes (case-insensitive) |
| -------------- | -------------------------------------- |
| System.Byte | `System.Byte`, `byte`, `uint8` |
| System.Int16 | `System.Int16`, `short`, `int16` |
| System.Int32 | `System.Int32`, `int`, `int32` |
| System.Int64 | `System.Int64`, `long`, `int64` |
| System.Boolean | `System.Boolean`, `bool` |
| System.String | `System.String`, `string` |
8 changes: 8 additions & 0 deletions docs/SdkDiagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- [NerdbankGitVersioning module (2000-2099)](#nerdbankgitversioning-module-2000-2099)
- [ReleaseAssetList module (2100-2199)](#releaseassetlist-module-2100-2199)
- [Wine module (2200-2299)](#wine-module-2200-2299)
- [ThisAssemblyClass module (2300-2399)](#thisassemblyclass-module-2300-2399)

## Overview

Expand Down Expand Up @@ -123,3 +124,10 @@ This module has no associated diagnostics.
| Code | Severity | Message | Description |
| --------- | :------: | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| BVSDK2200 | Error | One or more tools need Wine to run on this system, but no Wine invocation command has been defined: ...[;...] | One or more tools needed to build and/or distribute your project need [Wine](https://winehq.org) to run under a non-Windows operating system. In order to use Wine with Buildvana SDK, the `WineInvocationCommand` property must be set as explained in [the module documentation](./modules/Wine.md#configuration). |

## ThisAssemblyClass module (2300-2399)

| Code | Severity | Message | Description |
| --------- | :------: | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| BVSDK2300 | Warning | ThisAssembly class generation is only supported in C# projects. | Property `GenerateThisAssemblyClass` was set to `true` in a project whose language is not C#. No `ThisAssembly` class will be generated. |
| BVSDK2301 | Error | Constant '...' has invalid value '...'. | A `ThisAssemblyConstant` item has a `Value` metadata that cannot be parsed according to the [constants syntax](ConstantsSyntax.md): an unknown type prefix, or a value that cannot be parsed as the specified type. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

### New Rules

Rule ID | Category | Severity | Notes
----------|---------------|----------|--------------------------------------------------------------
BVSDK2301 | Buildvana.Sdk | Error | ThisAssemblyClassGenerator: invalid ThisAssemblyConstant value
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="Buildvana.Sdk.SourceGenerators.Tests" />
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" PrivateAssets="all" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,19 @@

namespace Buildvana.Sdk.SourceGenerators.Internal;

/// <summary>
/// Provides extension methods for <c>AnalyzerConfigOptionsProvider</c> instances.
/// </summary>
#pragma warning disable CA1034 // Nested types should not be visible — false positive on C# 14 extension blocks; fixed in .NET 11, backport to .NET 10 requested in https://github.com/dotnet/sdk/issues/53984
#pragma warning disable CA1708 // Identifiers should differ by more than case — false positive on classes with C# 14 extension blocks; fixed in .NET 11, https://github.com/dotnet/sdk/issues/51716
internal static class AnalyzerConfigOptionsProviderExtensions
{
public static bool? GetBooleanMSBuildProperty(this AnalyzerConfigOptionsProvider @this, string name)
=> @this.GlobalOptions.TryGetValue($"build_property.{name}", out var value) ? value.Equals("true", StringComparison.OrdinalIgnoreCase) : null;
extension(AnalyzerConfigOptionsProvider @this)
{
public bool? GetBooleanMSBuildProperty(string name)
=> @this.GlobalOptions.TryGetValue($"build_property.{name}", out var value) ? value.Equals("true", StringComparison.OrdinalIgnoreCase) : null;

public string? GetMSBuildProperty(string name)
=> @this.GlobalOptions.TryGetValue($"build_property.{name}", out var value) ? value : null;
}
}
101 changes: 101 additions & 0 deletions src/Buildvana.Sdk.SourceGenerators/Internal/ConstantValueParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Globalization;

namespace Buildvana.Sdk.SourceGenerators.Internal;

/// <summary>
/// Parses constant values expressed in the syntax documented in <c>docs/ConstantsSyntax.md</c>:
/// an empty string yields <see langword="null"/>; a double-quoted string (with inner double quotes doubled)
/// yields the quoted text; a <c>type:value</c> pair yields a value of the specified type;
/// anything else is parsed by guessing the type (int, then long, then bool, then string).
/// </summary>
internal static class ConstantValueParser
{
private static readonly Dictionary<string, Type> AllowedTypes = new(StringComparer.OrdinalIgnoreCase)
{
["System.Byte"] = typeof(byte),
["byte"] = typeof(byte),
["uint8"] = typeof(byte),
["System.Int16"] = typeof(short),
["short"] = typeof(short),
["int16"] = typeof(short),
["System.Int32"] = typeof(int),
["int"] = typeof(int),
["int32"] = typeof(int),
["System.Int64"] = typeof(long),
["long"] = typeof(long),
["int64"] = typeof(long),
["System.Boolean"] = typeof(bool),
["bool"] = typeof(bool),
["System.String"] = typeof(string),
["string"] = typeof(string),
};

public static bool TryParse(string? str, out object? result)
{
if (string.IsNullOrEmpty(str))
{
result = null;
return true;
}

if (str!.Length > 1 && str[0] == '"' && str[^1] == '"')
{
result = str.Substring(1, str.Length - 2).Replace("\"\"", "\"");
return true;
}

var colonPos = str.IndexOf(':');
return colonPos < 1
? TryParseGuessingType(str, out result)
: TryParseTyped(str.Substring(0, colonPos), str.Substring(colonPos + 1), out result);
}

private static bool TryParseGuessingType(string str, out object? result)
{
if (int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedInt))
{
result = parsedInt;
return true;
}

if (long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLong))
{
result = parsedLong;
return true;
}

if (bool.TryParse(str, out var parsedBool))
{
result = parsedBool;
return true;
}

result = str;
return true;
}

private static bool TryParseTyped(string typeStr, string str, out object? result)
{
if (!AllowedTypes.TryGetValue(typeStr.Trim(), out var type))
{
result = null;
return false;
}

try
{
result = Convert.ChangeType(str, type, CultureInfo.InvariantCulture);
return true;
}
catch (Exception e) when (e is FormatException or OverflowException or InvalidCastException)
{
result = null;
return false;
}
}
}
Loading