Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support relative paths in interceptors #71879

Merged
merged 19 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
19 changes: 6 additions & 13 deletions docs/features/interceptors.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,13 @@ File-local declarations of this type (`file class InterceptsLocationAttribute`)

#### File paths

File paths used in `[InterceptsLocation]` are expected to have `/pathmap` substitution already applied. Generators should accomplish this by locally recreating the file path transformation performed by the compiler:
The *referenced syntax tree* of an `[InterceptsLocationAttribute]` is determined using the following rules:
1. A *mapped path* of each syntax tree is determined by applying [`/pathmap`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.commandlinearguments.pathmap?view=roslyn-dotnet-4.7.0) substitution to `SyntaxTree.FilePath`.
2. For a given `[InterceptsLocation]` usage, the `filePath` argument value is compared to the *mapped path* of each syntax tree using ordinal string comparison. If exactly one syntax tree matches under this comparison, that is the *referenced syntax tree*.
jaredpar marked this conversation as resolved.
Show resolved Hide resolved
3. If no syntax tree matched by the above comparison, then the `filePath` argument value is resolved relative to path of the containing syntax tree of the `[InterceptsLocation]` usage. Pathmap substitution is applied to the result of this relative resolution. If exactly one syntax tree matches this resolved and mapped path, that is the *referenced syntax tree*.
4. If neither of the above methods of comparison matched exactly one syntax tree, an error occurs.
Copy link
Member

Choose a reason for hiding this comment

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

Couldn't this be summarized as

The filePath argument is interepreted the same as a path for a #line directive in the same location?

If not in what ways are they different?


```cs
using Microsoft.CodeAnalysis;

string GetInterceptorFilePath(SyntaxTree tree, Compilation compilation)
{
return compilation.Options.SourceReferenceResolver?.NormalizePath(tree.FilePath, baseFilePath: null) ?? tree.FilePath;
}
```

The file path given in the attribute must be equal by ordinal comparison to the value given by the above function.

The compiler does not map `#line` directives when determining if an `[InterceptsLocation]` attribute intercepts a particular call in syntax.
Note that `#line` directives are not considered when determining the call referenced by an `[InterceptsLocation]` attribute.
Copy link
Member

Choose a reason for hiding this comment

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

The doc still says that we consider mapped path here, thought we decided it would just match #line

Copy link
Contributor Author

@RikkiGibson RikkiGibson Feb 23, 2024

Choose a reason for hiding this comment

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

The term mapped path is used in reference to /pathmap, not #line mapping. This sentence is meant to say that presence or absence of #line in source code doesn't affect what an InterceptsLocationAttribute means (aside from the trailing newline affecting the line numbers of the subsequent lines).

I think this is something which will become much simpler conceptually as we phase out the "compat" lookup. Basically, there will no longer be any expectation for the user of [InterceptsLocation] to perform any kind of mapping of the file path, line, or column; rather, to just get and use a relative path from the "current file" to the "referenced file", and to use the "unmapped" line and column numbers.

Copy link
Member

@jaredpar jaredpar Feb 23, 2024

Choose a reason for hiding this comment

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

Point (2) still says that path map impacts how we resolve paths in InterceptsLocation. That means the following paths do not necessarily mean the same thing.

#line /src/blah/code.cs
[Intercepts("/src/blah/code.cs")]

That feels very broken. Both of these features are about mapping a file path, relative or absolute, to another file in the compilation. Why should one use /pathmap and the other not use it?

If I'm incorrect and #line does respect path map for embedding I'm happy to be corrected on this point. If it does though why do we have such a complex section on how this mapping works. Why can't we just simply say:

The path inside InterceptsLocation is resolved the same way a #line would be


#### Position

Expand Down
4 changes: 2 additions & 2 deletions src/Compilers/CSharp/Portable/CommandLine/CSharpCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,9 +373,9 @@ protected override void ResolveEmbeddedFilesFromExternalSourceDirectives(
}
}

private protected override GeneratorDriver CreateGeneratorDriver(ParseOptions parseOptions, ImmutableArray<ISourceGenerator> generators, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, ImmutableArray<AdditionalText> additionalTexts)
private protected override GeneratorDriver CreateGeneratorDriver(string baseDirectory, ParseOptions parseOptions, ImmutableArray<ISourceGenerator> generators, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, ImmutableArray<AdditionalText> additionalTexts)
{
return CSharpGeneratorDriver.Create(generators, additionalTexts, (CSharpParseOptions)parseOptions, analyzerConfigOptionsProvider);
return CSharpGeneratorDriver.Create(generators, additionalTexts, (CSharpParseOptions)parseOptions, analyzerConfigOptionsProvider, driverOptions: new GeneratorDriverOptions() { BaseDirectory = baseDirectory });
}

private protected override void DiagnoseBadAccesses(TextWriter consoleOutput, ErrorLogger? errorLogger, Compilation compilation, ImmutableArray<Diagnostic> diagnostics)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1057,10 +1057,10 @@ internal OneOrMany<SyntaxTree> GetSyntaxTreesByMappedPath(string mappedPath)
ImmutableSegmentedDictionary<string, OneOrMany<SyntaxTree>> computeMappedPathToSyntaxTree()
{
var builder = ImmutableSegmentedDictionary.CreateBuilder<string, OneOrMany<SyntaxTree>>();
var resolver = Options.SourceReferenceResolver;
var resolver = Options.SourceReferenceResolver ?? SourceFileResolver.Default;
RikkiGibson marked this conversation as resolved.
Show resolved Hide resolved
foreach (var tree in SyntaxTrees)
{
var path = resolver?.NormalizePath(tree.FilePath, baseFilePath: null) ?? tree.FilePath;
var path = resolver.NormalizePath(tree.FilePath, baseFilePath: null) ?? tree.FilePath;
builder[path] = builder.ContainsKey(path) ? builder[path].Add(tree) : OneOrMany.Create(tree);
}
return builder.ToImmutable();
Expand Down
2 changes: 1 addition & 1 deletion src/Compilers/CSharp/Portable/Errors/ErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2202,7 +2202,7 @@ internal enum ErrorCode
ERR_InterceptorLineOutOfRange = 9142,
ERR_InterceptorCharacterOutOfRange = 9143,
ERR_InterceptorSignatureMismatch = 9144,
ERR_InterceptorPathNotInCompilationWithUnmappedCandidate = 9145,
// ERR_InterceptorPathNotInCompilationWithUnmappedCandidate = 9145,
ERR_InterceptorMethodMustBeOrdinary = 9146,
ERR_InterceptorMustReferToStartOfTokenPosition = 9147,
ERR_InterceptorMustHaveMatchingThisParameter = 9148,
Expand Down
1 change: 0 additions & 1 deletion src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2348,7 +2348,6 @@ internal static bool IsBuildOnlyDiagnostic(ErrorCode code)
case ErrorCode.ERR_InterceptorContainingTypeCannotBeGeneric:
case ErrorCode.ERR_InterceptorPathNotInCompilation:
case ErrorCode.ERR_InterceptorPathNotInCompilationWithCandidate:
case ErrorCode.ERR_InterceptorPathNotInCompilationWithUnmappedCandidate:
case ErrorCode.ERR_InterceptorPositionBadToken:
case ErrorCode.ERR_InterceptorLineOutOfRange:
case ErrorCode.ERR_InterceptorCharacterOutOfRange:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1000,29 +1000,31 @@ private void DecodeInterceptsLocationAttribute(DecodeWellKnownAttributeArguments
return;
}

var syntaxTrees = DeclaringCompilation.SyntaxTrees;
var referenceResolver = DeclaringCompilation.Options.SourceReferenceResolver ?? SourceFileResolver.Default;
RikkiGibson marked this conversation as resolved.
Show resolved Hide resolved

// First, the attributeFilePath might be a mapped path of one of the trees in the compilation.
// In this case, it's not intended to be resolved relative to anything, and we won't know if this is the case until we look it up.
var matchingTrees = DeclaringCompilation.GetSyntaxTreesByMappedPath(attributeFilePath);
if (matchingTrees.Count == 0
&& referenceResolver.NormalizePath(attributeFilePath, baseFilePath: SyntaxTree.FilePath) is { } normalizedFilePath)
{
// Try again but normalize the given file path relative to the containing file of the attribute application
matchingTrees = DeclaringCompilation.GetSyntaxTreesByMappedPath(normalizedFilePath);

// Use the normalized path in the following diagnostic messages so the user has a hint of where we are searching.
// Relative paths are the happy path so we want them to know how the compiler has resolved the path they gave us.
attributeFilePath = normalizedFilePath;
}

if (matchingTrees.Count == 0)
{
var referenceResolver = DeclaringCompilation.Options.SourceReferenceResolver;
// if we expect '/_/Program.cs':

// we might get: 'C:\Project\Program.cs' <-- path not mapped
var unmappedMatch = syntaxTrees.FirstOrDefault(static (tree, filePath) => tree.FilePath == filePath, attributeFilePath);
if (unmappedMatch != null)
{
diagnostics.Add(
ErrorCode.ERR_InterceptorPathNotInCompilationWithUnmappedCandidate,
attributeData.GetAttributeArgumentLocation(filePathParameterIndex),
attributeFilePath,
mapPath(referenceResolver, unmappedMatch));
return;
}

// we might get: '\_\Program.cs' <-- slashes not normalized
// we might get: '\_/Program.cs' <-- slashes don't match
// we might get: 'Program.cs' <-- suffix match
// Force normalization of all '\' to '/', but when we recommend a path in the diagnostic message, ensure it will match what we expect if the user decides to use it.
var syntaxTrees = DeclaringCompilation.SyntaxTrees;
var suffixMatch = syntaxTrees.FirstOrDefault(static (tree, pair)
=> mapPath(pair.referenceResolver, tree)
.Replace('\\', '/')
Expand Down
Loading
Loading