Skip to content
Open
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
3 changes: 3 additions & 0 deletions eng/targets/Settings.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
<!-- RestoreUseStaticGraphEvaluation will cause prebuilts when building source-only. -->
<RestoreUseStaticGraphEvaluation Condition="'$(DotNetBuildSourceOnly)' != 'true'">true</RestoreUseStaticGraphEvaluation>

<!-- TODO2: use an officially published package and delete this. -->
<RestoreAdditionalProjectSources>F:\src\dotnet-sdk\artifacts\packages\Debug\NonShipping</RestoreAdditionalProjectSources>

<!-- Disable the implicit nuget fallback folder as it makes it hard to locate and copy ref assemblies to the test output folder -->
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
<ToolsetPackagesDir>$(RepoRoot)build\ToolsetPackages\</ToolsetPackagesDir>
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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: [Rule],
generatedCodeAnalysisFlags: GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics)
{
public const string DiagnosticId = "FileBasedPrograms";

#pragma warning disable RS0030 // Do not use banned APIs
// We would use 'AbstractCodeQualityDiagnosticAnalyzer.CreateDescriptor()', but, we want these diagnostics to have error severity.
private static readonly DiagnosticDescriptor Rule = new(
id: DiagnosticId,
title: DiagnosticId,
messageFormat: "{0}",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
helpLinkUri: "https://learn.microsoft.com/dotnet/csharp/language-reference/preprocessor-directives#file-based-apps",
// Note that this is an "editor-only" analyzer.
// When building or running file-based apps, the dotnet cli uses its own process to report errors on file-level directives.
customTags: DiagnosticCustomTags.Create(isUnnecessary: false, isConfigurable: false, isCustomConfigurable: false, enforceOnBuild: EnforceOnBuild.Never));
#pragma warning restore RS0030 // Do not use banned APIs

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

protected override void InitializeWorker(AnalysisContext context)
{
context.RegisterCompilationStartAction(context =>
{
context.RegisterSyntaxTreeAction(visitSyntaxTree);
});

void visitSyntaxTree(SyntaxTreeAnalysisContext context)
{
var tree = context.Tree;
if (!tree.Options.Features.ContainsKey("FileBasedProgram"))
return;

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

// App directives are only valid when they appear before the first C# token
var rootLeadingTrivia = root.GetLeadingTrivia();
var diagnosticBag = DiagnosticBag.Collect(out var diagnosticsBuilder);
FileLevelDirectiveHelpers.FindLeadingDirectives(
new SourceFile(tree.FilePath, tree.GetText(context.CancellationToken)),
root.GetLeadingTrivia(),
diagnosticBag,
builder: null);

foreach (var diag in diagnosticsBuilder)
{
context.ReportDiagnostic(createDiagnostic(tree, diag));
}

// 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
}

Diagnostic createDiagnostic(SyntaxTree syntaxTree, SimpleDiagnostic simpleDiagnostic)
{
return Diagnostic.Create(
Rule,
location: Location.Create(syntaxTree, simpleDiagnostic.Location.TextSpan),
simpleDiagnostic.Message);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,17 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Humanizer.Core" PrivateAssets="compile" />

<!-- TODO2: central package version. Use published version. -->
<PackageReference Include="Microsoft.DotNet.FileBasedPrograms" VersionOverride="10.0.200-dev" GeneratePathProperty="true" />
<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>
</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 @@ -152,8 +152,8 @@ 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;
}
}

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);
Expand Down
Loading