Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions eng/Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@
<PackageVersion Include="vswhere" Version="$(vswhereVersion)" />
<PackageVersion Include="CliWrap" Version="3.8.2" />
<PackageVersion Include="Microsoft.DotNet.DarcLib" Version="$(MicrosoftDotNetDarcLibPackageVersion)" />
<PackageVersion Include="Microsoft.DotNet.FileBasedPrograms" Version="$(MicrosoftDotNetFileBasedProgramsPackageVersion)" />
<PackageVersion Include="Spectre.Console" Version="0.51.1" />
<!--
The version of Roslyn we build Source Generators against that are built in this
Expand Down
2 changes: 2 additions & 0 deletions eng/Version.Details.props
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ This file should be imported by eng/Versions.props
<MicrosoftDiaSymReaderPackageVersion>2.0.0</MicrosoftDiaSymReaderPackageVersion>
<!-- dotnet/arcade-services dependencies -->
<MicrosoftDotNetDarcLibPackageVersion>1.1.0-beta.25503.1</MicrosoftDotNetDarcLibPackageVersion>
<!-- dotnet/sdk dependencies -->
<MicrosoftDotNetFileBasedProgramsPackageVersion>10.0.200-preview.0.25556.104</MicrosoftDotNetFileBasedProgramsPackageVersion>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably setup a subscription so that we get pushed updates for this package automatically, but, want to start by just taking the version that was pushed to the feed yesterday.

<!-- dotnet/roslyn-analyzers dependencies -->
<MicrosoftCodeAnalysisAnalyzersPackageVersion>3.11.0</MicrosoftCodeAnalysisAnalyzersPackageVersion>
<MicrosoftCodeAnalysisAnalyzerUtilitiesPackageVersion>3.3.0</MicrosoftCodeAnalysisAnalyzerUtilitiesPackageVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,17 @@ protected static DiagnosticDescriptor CreateDescriptor(
bool isUnnecessary,
bool isEnabledByDefault = true,
bool isConfigurable = true,
LocalizableString? description = null)
LocalizableString? description = null,
DiagnosticSeverity defaultSeverity = DiagnosticSeverity.Info,
string? helpLinkUri = null)
#pragma warning disable RS0030 // Do not use banned APIs
=> new(
id, title, messageFormat,
DiagnosticCategory.CodeQuality,
DiagnosticSeverity.Info,
defaultSeverity,
isEnabledByDefault,
description,
helpLinkUri: DiagnosticHelper.GetHelpLinkForDiagnosticId(id),
helpLinkUri: helpLinkUri ?? DiagnosticHelper.GetHelpLinkForDiagnosticId(id),
customTags: DiagnosticCustomTags.Create(isUnnecessary, isConfigurable, isCustomConfigurable: hasAnyCodeStyleOption, enforceOnBuild));
#pragma warning restore RS0030 // Do not use banned APIs
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,18 @@ private static void ValidateHelpLinkForDiagnostic(string diagnosticId, string he
return;
}

if (diagnosticId == "EnableGenerateDocumentationFile")
if (diagnosticId == FileBasedPrograms.FileLevelDirectiveDiagnosticAnalyzer.DiagnosticId)
{
Assert.Equal("https://github.com/dotnet/roslyn/issues/41640", helpLinkUri);
return;
}

if (diagnosticId == "FileBasedPrograms")
{
Assert.Equal("https://learn.microsoft.com/dotnet/csharp/language-reference/preprocessor-directives#file-based-apps", helpLinkUri);
return;
}

if (helpLinkUri != $"https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/{diagnosticId.ToLowerInvariant()}")
{
Assert.True(false, $"Invalid help link for {diagnosticId}");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#if !NET
using Roslyn.Utilities;

namespace Microsoft.DotNet.FileBasedPrograms;

/// <summary>Provides implementations of certain methods to the FileBasedPrograms source package.</summary>
internal partial class ExternalHelpers
{
public static partial int CombineHashCodes(int value1, int value2)
=> Hash.Combine(value1, value2);

public static partial string GetRelativePath(string relativeTo, string path)
=> PathUtilities.GetRelativePath(relativeTo, path);

public static partial bool IsPathFullyQualified(string path)
=> PathUtilities.IsAbsolute(path);
}
#endif
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the analyzer is included under this path, because we only expect/want it to run in the editor. Running it on the command line would just be redundant with what dotnet build app.cs already does.

It wouldn't cause a problem if it did somehow run on the command line, though, because in general, in a scenario where this analyzer would report an error, dotnet build app.cs would have reported an error at an earlier stage, and the compiler wouldn't run.

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.CodeAnalysis.CodeQuality;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.DotNet.FileBasedPrograms;

namespace Microsoft.CodeAnalysis.FileBasedPrograms;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class FileLevelDirectiveDiagnosticAnalyzer()
: AbstractCodeQualityDiagnosticAnalyzer(
descriptors: [s_descriptor],
generatedCodeAnalysisFlags: GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics)
{
public const string DiagnosticId = "FileBasedPrograms";

private static readonly DiagnosticDescriptor s_descriptor = CreateDescriptor(
id: DiagnosticId,
enforceOnBuild: EnforceOnBuild.Never,
title: DiagnosticId,
messageFormat: "{0}",
hasAnyCodeStyleOption: false,
isUnnecessary: false,
isEnabledByDefault: true,
isConfigurable: false,
defaultSeverity: DiagnosticSeverity.Error,
helpLinkUri: "https://learn.microsoft.com/dotnet/csharp/language-reference/preprocessor-directives#file-based-apps");

public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
=> DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis;

protected override void InitializeWorker(AnalysisContext context)
{
context.RegisterSyntaxTreeAction(context =>
{
var cancellationToken = context.CancellationToken;
var tree = context.Tree;
if (!tree.Options.Features.ContainsKey("FileBasedProgram"))
return;

var root = tree.GetRoot(cancellationToken);
if (!root.ContainsDirectives)
return;

// The compiler already reports an error on all the directives past the first token in the file.
// Therefore, the analyzer only deals with the directives on the first token.
// Console.WriteLine("Hello World!");
// #:property foo=bar // error CS9297: '#:' directives cannot be after first token in file
var diagnosticBag = DiagnosticBag.Collect(out var diagnosticsBuilder);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this type is unfamiliar to me. need to figure out what that is about.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FileLevelDirectiveHelpers.FindLeadingDirectives(
new SourceFile(tree.FilePath, tree.GetText(cancellationToken)),
root.GetLeadingTrivia(),
diagnosticBag,
builder: null);

foreach (var simpleDiagnostic in diagnosticsBuilder)
{
context.ReportDiagnostic(Diagnostic.Create(
s_descriptor,
location: Location.Create(tree, simpleDiagnostic.Location.TextSpan),
simpleDiagnostic.Message));
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[*.cs]
dotnet_diagnostic.RS0051.severity = none # Symbol is not part of the declared API
dotnet_diagnostic.RS0056.severity = none # InternalAPI.txt is missing '#nullable enable', so the nullability annotations of API isn't recorded. It is recommended to enable this tracking.
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,15 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Humanizer.Core" PrivateAssets="compile" />
<PackageReference Include="Microsoft.DotNet.FileBasedPrograms" GeneratePathProperty="true" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be PrivateAssets="All" ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<EmbeddedResource
Include="$(PkgMicrosoft_DotNet_FileBasedPrograms)\contentFiles\cs\any\FileBasedProgramsResources.resx"
GenerateSource="true"
Namespace="Microsoft.DotNet.FileBasedPrograms" />
</ItemGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants);FILE_BASED_PROGRAMS_SOURCE_PACKAGE_GRACEFUL_EXCEPTION</DefineConstants>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this for?

Copy link
Member Author

@RikkiGibson RikkiGibson Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dotnet/sdk#51487

Usage of GracefulException, and associated compilation symbol, should be removed: dotnet/sdk#51373 (comment)

This symbol causes the source package to declare type GracefulException which is currently used in the FileBasedPrograms implementation. We want to remove the use of this type from the source package once we have the time to do so.

</PropertyGroup>
<Import Project="..\..\..\Analyzers\CSharp\Analyzers\CSharpAnalyzers.projitems" Label="Shared" />
<Import Project="..\..\..\Analyzers\CSharp\CodeFixes\CSharpCodeFixes.projitems" Label="Shared" />
<Import Project="..\..\..\Compilers\CSharp\CSharpAnalyzerDriver\CSharpAnalyzerDriver.projitems" Label="Shared" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,9 @@ public async ValueTask<bool> TryRemoveMiscellaneousDocumentAsync(DocumentUri uri
};
}

protected override async ValueTask OnProjectUnloadedAsync(string projectFilePath)
protected override ValueTask OnProjectUnloadedAsync(string projectFilePath)
{
await _projectXmlProvider.UnloadCachedDiagnosticsAsync(projectFilePath);
return ValueTask.CompletedTask;
}

protected override async ValueTask TransitionPrimordialProjectToLoadedAsync(
Expand Down
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ripping out all the bits that were reporting "build only diagnostics" for #: directives.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,63 +22,9 @@ namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
[Export(typeof(VirtualProjectXmlProvider)), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal class VirtualProjectXmlProvider(IDiagnosticsRefresher diagnosticRefresher, DotnetCliHelper dotnetCliHelper)
internal class VirtualProjectXmlProvider(DotnetCliHelper dotnetCliHelper)
{
private readonly SemaphoreSlim _gate = new(initialCount: 1);
private readonly Dictionary<string, ImmutableArray<SimpleDiagnostic>> _diagnosticsByFilePath = [];

internal async ValueTask<ImmutableArray<SimpleDiagnostic>> GetCachedDiagnosticsAsync(string path, CancellationToken cancellationToken)
{
using (await _gate.DisposableWaitAsync(cancellationToken))
{
_diagnosticsByFilePath.TryGetValue(path, out var diagnostics);
return diagnostics;
}
}

internal async ValueTask UnloadCachedDiagnosticsAsync(string path)
{
using (await _gate.DisposableWaitAsync(CancellationToken.None))
{
_diagnosticsByFilePath.Remove(path);
}
}

internal async Task<(string VirtualProjectXml, ImmutableArray<SimpleDiagnostic> Diagnostics)?> GetVirtualProjectContentAsync(string documentFilePath, ILogger logger, CancellationToken cancellationToken)
{
var result = await GetVirtualProjectContentImplAsync(documentFilePath, logger, cancellationToken);
if (result is { } project)
{
using (await _gate.DisposableWaitAsync(cancellationToken))
{
_diagnosticsByFilePath.TryGetValue(documentFilePath, out var previousCachedDiagnostics);
_diagnosticsByFilePath[documentFilePath] = project.Diagnostics;

// check for difference, and signal to host to update if so.
if (previousCachedDiagnostics.IsDefault || !project.Diagnostics.SequenceEqual(previousCachedDiagnostics))
diagnosticRefresher.RequestWorkspaceRefresh();
}
}
else
{
using (await _gate.DisposableWaitAsync(CancellationToken.None))
{
if (_diagnosticsByFilePath.TryGetValue(documentFilePath, out var diagnostics))
{
_diagnosticsByFilePath.Remove(documentFilePath);
if (!diagnostics.IsDefaultOrEmpty)
{
// diagnostics have changed from "non-empty" to "unloaded". refresh.
diagnosticRefresher.RequestWorkspaceRefresh();
}
}
}
}

return result;
}

private async Task<(string VirtualProjectXml, ImmutableArray<SimpleDiagnostic> Diagnostics)?> GetVirtualProjectContentImplAsync(string documentFilePath, ILogger logger, CancellationToken cancellationToken)
{
var workingDirectory = Path.GetDirectoryName(documentFilePath);
var process = dotnetCliHelper.Run(["run-api"], workingDirectory, shouldLocalizeOutput: true, keepStandardInputOpen: true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we still need to call run-api?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would want to add a method to source package for getting a virtual project and use it here before getting rid of that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so we only wanted the source package for live diagnostics to be visible even without saving the file (which would not be easily possible with run-api). For other things, run-api still works fine. Is that correct?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I think it's a good idea to expand the source package, and use it to replace the task being done by this method, though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to using source package instead of calling the run-api.

Expand Down
Loading