Skip to content

Commit 262b31a

Browse files
authored
Use the full file path as the target path when it's missing (#12355)
This allows projects that don't use the Razor SDK (eg, #12332 or #12331) to have some kind of tooling experience. They won't have a perfect one, because there is no guarantee that the code the generator produces will compile (and in the first linked issue, it doesn't), but it will produce something and will have unique hint paths etc. Projects can essentially "enable tooling" by putting this in their project file: ```xml <ItemGroup Condition="'$(DesignTimeBuild)' == 'true'"> <!-- The cohosting editor uses the Razor source generator for VS functionality, so we have to make sure it can see the .cshtml files. --> <AdditionalFiles Include="**\*.cshtml" /> <!-- Now we have to make sure the generator is actually referenced. Luckily the Razor tooling will automatically redirect any reference to it, to the right location of the file that ships with VS, so we don't have to care where it actually is. --> <Analyzer Include="Microsoft.CodeAnalysis.Razor.Compiler.dll" /> <!-- Make sure the source generator knows where the project is, for computing target paths. --> <CompilerVisibleProperty Include="MSBuildProjectDirectory" /> </ItemGroup> ``` Source generator changes: * If there is a target path supplied, use it * If there is no target path supplied, but we know the project path, compute a target path * If there is no target path supplied, use the whole file path Tooling changes: * Instead of trying to have an algorithm perform similar logic and guess what the source generator would have done, we just look through the source generated documents and find a match. Full path matches always win, otherwise project relative, being careful to watch out for duplicates. The "use the whole file path" change is arguably separate, and it is TBD if it is the way we want to go. If we don't do that, it just makes the compiler visible property part of the above required.
2 parents 26a49b3 + da66289 commit 262b31a

File tree

15 files changed

+464
-272
lines changed

15 files changed

+464
-272
lines changed

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Microsoft.CodeAnalysis.Razor.Compiler.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
<InternalsVisibleTo Include="Microsoft.VisualStudio.LegacyEditor.Razor.Test" Key="$(RazorKey)" />
6969
<InternalsVisibleTo Include="Microsoft.VisualStudio.RazorExtension" Key="$(RazorKey)" />
7070
<InternalsVisibleTo Include="Microsoft.VisualStudioCode.RazorExtension" Key="$(RazorKey)" />
71+
<InternalsVisibleTo Include="Microsoft.VisualStudioCode.RazorExtension.Test" Key="$(RazorKey)" />
7172
</ItemGroup>
7273

7374
</Project>

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -77,25 +77,37 @@ private static (SourceGeneratorProjectItem?, Diagnostic?) ComputeProjectItems((A
7777
var (additionalText, globalOptions) = pair;
7878
var options = globalOptions.GetOptions(additionalText);
7979

80-
string relativePath;
81-
if (options.TryGetValue("build_metadata.AdditionalFiles.TargetPath", out var encodedRelativePath))
80+
string? relativePath = null;
81+
var hasTargetPath = options.TryGetValue("build_metadata.AdditionalFiles.TargetPath", out var encodedRelativePath);
82+
if (hasTargetPath && !string.IsNullOrWhiteSpace(encodedRelativePath))
8283
{
83-
// TargetPath is optional, but must have a value if provided.
84-
if (string.IsNullOrWhiteSpace(encodedRelativePath))
85-
{
86-
var diagnostic = Diagnostic.Create(
87-
RazorDiagnostics.TargetPathNotProvided,
88-
Location.None,
89-
additionalText.Path);
90-
return (null, diagnostic);
91-
}
92-
9384
relativePath = Encoding.UTF8.GetString(Convert.FromBase64String(encodedRelativePath));
9485
}
95-
else
86+
else if (globalOptions.GlobalOptions.TryGetValue("build_property.MSBuildProjectDirectory", out var projectPath) &&
87+
projectPath is { Length: > 0 } &&
88+
additionalText.Path.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase))
89+
{
90+
// Fallback, when TargetPath isn't specified but we know about the project directory, we can do our own calulation of
91+
// the project relative path, and use that as the target path. This is an easy way for a project that isn't using the
92+
// Razor SDK to still get TargetPath functionality without the complexity of specifying metadata on every item.
93+
relativePath = additionalText.Path[projectPath.Length..].TrimStart(['/', '\\']);
94+
}
95+
else if (!hasTargetPath)
9696
{
97-
// If the TargetPath is not provided, we effectively assume its in the root of the project.
98-
relativePath = Path.GetFileName(additionalText.Path);
97+
// If the TargetPath is not provided, it could be a Misc Files situation, or just a project that isn't using the
98+
// Web or Razor SDK. In this case, we just use the physical path.
99+
relativePath = additionalText.Path;
100+
}
101+
102+
if (relativePath is null)
103+
{
104+
// If we had a TargetPath but it was empty or whitespace, and we couldn't fall back to computing it from the project path
105+
// that's an error.
106+
var diagnostic = Diagnostic.Create(
107+
RazorDiagnostics.TargetPathNotProvided,
108+
Location.None,
109+
additionalText.Path);
110+
return (null, diagnostic);
99111
}
100112

101113
options.TryGetValue("build_metadata.AdditionalFiles.CssScope", out var cssScope);

src/Razor/src/Microsoft.CodeAnalysis.Razor.CohostingShared/Diagnostics/CohostDocumentPullDiagnosticsEndpointBase.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,7 @@ protected virtual TRequest CreateHtmlParams(Uri uri)
101101

102102
protected static Task<SourceGeneratedDocument?> TryGetGeneratedDocumentAsync(TextDocument razorDocument, CancellationToken cancellationToken)
103103
{
104-
if (!razorDocument.TryComputeHintNameFromRazorDocument(out var hintName))
105-
{
106-
return SpecializedTasks.Null<SourceGeneratedDocument>();
107-
}
108-
109-
return razorDocument.Project.TryGetSourceGeneratedDocumentFromHintNameAsync(hintName, cancellationToken);
104+
return razorDocument.Project.TryGetSourceGeneratedDocumentForRazorDocumentAsync(razorDocument, cancellationToken);
110105
}
111106

112107
private async Task<LspDiagnostic[]> GetCSharpDiagnosticsAsync(TextDocument razorDocument, Guid correletionId, CancellationToken cancellationToken)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ProjectExtensions.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
using System.Linq;
99
using System.Threading;
1010
using System.Threading.Tasks;
11+
using Microsoft.AspNetCore.Razor;
1112
using Microsoft.AspNetCore.Razor.Language;
1213
using Microsoft.AspNetCore.Razor.PooledObjects;
1314
using Microsoft.AspNetCore.Razor.Threading;
1415
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
1516
using Microsoft.CodeAnalysis.Razor;
1617
using Microsoft.CodeAnalysis.Razor.Telemetry;
18+
using Microsoft.NET.Sdk.Razor.SourceGenerators;
1719

1820
namespace Microsoft.CodeAnalysis;
1921

@@ -103,6 +105,76 @@ private static ImmutableArray<ITagHelperDescriptorProvider> GetTagHelperDescript
103105
return generatedDocuments.SingleOrDefault(d => d.HintName == hintName);
104106
}
105107

108+
/// <summary>
109+
/// Finds source generated documents by iterating through all of them. In OOP there are better options!
110+
/// </summary>
111+
public static async Task<SourceGeneratedDocument?> TryGetSourceGeneratedDocumentForRazorDocumentAsync(this Project project, TextDocument razorDocument, CancellationToken cancellationToken)
112+
{
113+
if (razorDocument.FilePath is null)
114+
{
115+
return null;
116+
}
117+
118+
var generatedDocuments = await project.GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false);
119+
120+
// For misc files, and projects that don't have a globalconfig file (eg, non Razor SDK projects), the hint name will be based
121+
// on the full path of the file.
122+
var fullPathHintName = RazorSourceGenerator.GetIdentifierFromPath(razorDocument.FilePath);
123+
// For normal Razor SDK projects, the hint name will be based on the project-relative path of the file.
124+
var projectRelativeHintName = GetProjectRelativeHintName(razorDocument);
125+
126+
SourceGeneratedDocument? candidateDoc = null;
127+
foreach (var doc in generatedDocuments)
128+
{
129+
if (!doc.IsRazorSourceGeneratedDocument())
130+
{
131+
continue;
132+
}
133+
134+
if (doc.HintName == fullPathHintName)
135+
{
136+
// If the full path matches, we've found it for sure
137+
return doc;
138+
}
139+
else if (doc.HintName == projectRelativeHintName)
140+
{
141+
if (candidateDoc is not null)
142+
{
143+
// Multiple documents with the same hint name found, can't be sure which one to return
144+
// This can happen as a result of a bug in the source generator: https://github.com/dotnet/razor/issues/11578
145+
candidateDoc = null;
146+
break;
147+
}
148+
149+
candidateDoc = doc;
150+
}
151+
}
152+
153+
return candidateDoc;
154+
155+
static string? GetProjectRelativeHintName(TextDocument razorDocument)
156+
{
157+
var filePath = razorDocument.FilePath.AsSpanOrDefault();
158+
if (string.IsNullOrEmpty(razorDocument.Project.FilePath))
159+
{
160+
// Misc file - no project info to get a relative path
161+
return null;
162+
}
163+
164+
var projectFilePath = razorDocument.Project.FilePath.AsSpanOrDefault();
165+
var projectBasePath = PathUtilities.GetDirectoryName(projectFilePath);
166+
if (filePath.Length <= projectBasePath.Length)
167+
{
168+
// File must be from outside the project directory
169+
return null;
170+
}
171+
172+
var relativeDocumentPath = filePath[projectBasePath.Length..].TrimStart(['/', '\\']);
173+
174+
return RazorSourceGenerator.GetIdentifierFromPath(relativeDocumentPath);
175+
}
176+
}
177+
106178
/// <summary>
107179
/// Finds source generated documents by iterating through all of them. In OOP there are better options!
108180
/// </summary>

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/TextDocumentExtensions.cs

Lines changed: 0 additions & 51 deletions
This file was deleted.

0 commit comments

Comments
 (0)