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
5 changes: 5 additions & 0 deletions .github/workflows/dotnet-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ jobs:
shell: pwsh
run: scripts/make-changelog.ps1 "${{ env.VERSION }}" "${{ github.sha }}"

- name: Update Analyzer Releases
if: ${{ env.SHOULD_RELEASE == 'True' }}
shell: pwsh
run: scripts/make-analyzer-releases.ps1

- name: Commit Metadata
if: ${{ env.SHOULD_RELEASE == 'True' }}
shell: pwsh
Expand Down
9 changes: 7 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,14 @@ The SDK automatically detects project types based on naming conventions:

Properties set based on detection: `IsPrimaryProject`, `IsCliProject`, `IsAppProject`, `IsTestProject`

### Automatic Project References
### Analyzer-Enforced Requirements

Non-primary projects automatically reference the primary project if it exists. Test projects automatically get `InternalsVisibleTo` access from the projects they test.
The SDK uses Roslyn analyzers to enforce proper project configuration:

- **KTSU0001 (Error)**: Projects must include required standard packages (SourceLink, Polyfill, System.Memory, System.Threading.Tasks.Extensions). Requirements vary based on project type and target framework.
- **KTSU0002 (Error)**: Projects must expose internals to test projects using `[assembly: InternalsVisibleTo(...)]`. A code fixer is available to automatically add this attribute.

These properties are passed to analyzers via `CompilerVisibleProperty`: `IsTestProject`, `TestProjectExists`, `TestProjectNamespace`, `TargetFramework`, `TargetFrameworkIdentifier`.

### Metadata File Integration

Expand Down
113 changes: 0 additions & 113 deletions Directory.Build.props

This file was deleted.

21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ For a GUI application:

### 🔧 **Development Workflow**

- **Automatic Project References**: Smart cross-project referencing based on project types
- **Internals Visibility**: Automatic InternalsVisibleTo configuration for test projects
- **Analyzer-Enforced Requirements**: Roslyn analyzers (KTSU0001/KTSU0002) ensure proper package dependencies and internals visibility with helpful diagnostics and code fixers
- **Internals Visibility**: Code fixer to easily add InternalsVisibleTo attributes for test projects
- **GitHub Integration**: Built-in support for GitHub workflows and CI/CD
- **Cross-Platform Support**: Compatible with Windows, macOS, and Linux
- **Documentation Generation**: Automated XML documentation file generation
Expand Down Expand Up @@ -229,14 +229,21 @@ This enables the SDK to work with any nested project structure without configura

## Advanced Configuration Features

### Automatic Project References
### Analyzer-Enforced Requirements

Projects automatically reference the primary project and expose internals to test projects. Cross-references are intelligently configured based on project types and naming conventions.
The SDK includes Roslyn analyzers that enforce proper project configuration with helpful diagnostics and code fixers:

For example:
**KTSU0001 (Error)**: Projects must include required standard packages
- Enforces SourceLink packages (GitHub, Azure Repos)
- Enforces Polyfill package for non-test projects
- Enforces compatibility packages (System.Memory, System.Threading.Tasks.Extensions) based on target framework
- Diagnostic message includes package name and version number

- Non-primary projects automatically get `<ProjectReference>` to the primary project
- Primary project automatically exposes internals to test projects via `<InternalsVisibleTo>`
**KTSU0002 (Error)**: Projects must expose internals to test projects
- Code fixer automatically adds `[assembly: InternalsVisibleTo(...)]` attribute
- Use Ctrl+. (Quick Actions) to apply the fix

These analyzers ensure consistent project structure while giving you explicit control over dependencies.

### Available Properties

Expand Down
114 changes: 114 additions & 0 deletions Sdk.Analyzers/AddInternalsVisibleToAttributeCodeFixProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace ktsu.Sdk.Analyzers;

using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

/// <summary>
/// Code fix provider that adds InternalsVisibleToAttribute to expose internals to test projects
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddInternalsVisibleToAttributeCodeFixProvider))]
[Shared]
public class AddInternalsVisibleToAttributeCodeFixProvider : CodeFixProvider
{

/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds => [MissingInternalsVisibleToAttributeAnalyzer.DiagnosticId];

/// <inheritdoc/>
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

/// <inheritdoc/>
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null)
{
return;
}

Diagnostic diagnostic = context.Diagnostics.First();

context.RegisterCodeFix(
CodeAction.Create(
title: "Add [assembly: InternalsVisibleTo(...)]",
createChangedDocument: ct => AddInternalsVisibleToAttributeAsync(context.Document, diagnostic, ct),
equivalenceKey: nameof(AddInternalsVisibleToAttributeCodeFixProvider)),
diagnostic);
}

private static async Task<Document> AddInternalsVisibleToAttributeAsync(
Document document,
Diagnostic diagnostic,
CancellationToken cancellationToken)
{
SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (root is not CompilationUnitSyntax compilationUnit)
{
return document;
}

// Get test project namespace from analyzer config options

AnalyzerConfigOptions options = document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions;
if (!options.TryGetValue("build_property.TestProjectNamespace", out string? testNamespace) || string.IsNullOrWhiteSpace(testNamespace))
{
return document;
}

// Check if using directive already exists

bool hasUsing = compilationUnit.Usings.Any(u =>
u.Name?.ToString() == "System.Runtime.CompilerServices");

// Create the using directive if needed

SyntaxList<UsingDirectiveSyntax> newUsings = compilationUnit.Usings;
if (!hasUsing)
{
UsingDirectiveSyntax usingDirective = SyntaxFactory.UsingDirective(
SyntaxFactory.ParseName("System.Runtime.CompilerServices"))
.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);
newUsings = newUsings.Add(usingDirective);
}

// Create the InternalsVisibleTo attribute

AttributeArgumentSyntax attributeArgument = SyntaxFactory.AttributeArgument(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(testNamespace)));

AttributeSyntax attribute = SyntaxFactory.Attribute(
SyntaxFactory.ParseName("System.Runtime.CompilerServices.InternalsVisibleTo"),
SyntaxFactory.AttributeArgumentList(
SyntaxFactory.SingletonSeparatedList(attributeArgument)));

AttributeListSyntax attributeList = SyntaxFactory.AttributeList(
SyntaxFactory.AttributeTargetSpecifier(SyntaxFactory.Token(SyntaxKind.AssemblyKeyword)),
SyntaxFactory.SingletonSeparatedList(attribute))
.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);

// Add the attribute to the compilation unit

CompilationUnitSyntax newCompilationUnit = compilationUnit
.WithUsings(newUsings)
.AddAttributeLists(attributeList)
.WithLeadingTrivia(compilationUnit.GetLeadingTrivia())
.WithTrailingTrivia(compilationUnit.GetTrailingTrivia());

return document.WithSyntaxRoot(newCompilationUnit);
}
}
12 changes: 12 additions & 0 deletions Sdk.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

## Release {version}

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
KTSU0001 | ktsu.Sdk | Error | Missing required package reference
KTSU0002 | ktsu.Sdk | Error | Missing InternalsVisibleTo attribute for test project

7 changes: 7 additions & 0 deletions Sdk.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
; Unshipped analyzer releases
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
19 changes: 19 additions & 0 deletions Sdk.Analyzers/KtsuAnalyzerBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace ktsu.Sdk.Analyzers;

using Microsoft.CodeAnalysis.Diagnostics;

/// <summary>
/// Base class for all ktsu.Sdk analyzers
/// </summary>
public abstract class KtsuAnalyzerBase : DiagnosticAnalyzer
{

/// <summary>
/// Category for ktsu.Sdk analyzers
/// </summary>
protected const string Category = "ktsu.Sdk";
}
Loading