-
Notifications
You must be signed in to change notification settings - Fork 466
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
Add an analyzer for PublishSingleFile #3921
Conversation
@vitek-karas @sbomer @mateoatr @tlakollo @samsp-msft Can you all take a look at this and tell me what you think? There are certainly some instances with potential false positives (as I included in my test case), but it seems like this mistake is so easy to make and this scenario is so new that it would be better to produce extra warnings up front. |
When publishing single-file applications, certain APIs are very likely to return incorrect results. Most notably, asking for the Location of any Assembly loaded from the single-file will return an empty string, instead of a file path. To minimize suprise, this analyzer flags all usages of the APIs most likely to break when used in single-file when the PublishSingleFile property is set to true, and the IncludeAllContentForSelfExtractProperty is not true (which provides the legacy API behavior).
Codecov Report
@@ Coverage Diff @@
## master #3921 +/- ##
==========================================
- Coverage 95.62% 95.61% -0.01%
==========================================
Files 1151 1151
Lines 254095 253903 -192
Branches 15223 15209 -14
==========================================
- Hits 242977 242773 -204
- Misses 9180 9182 +2
- Partials 1938 1948 +10 |
@@ -1422,4 +1422,13 @@ | |||
<data name="DoNotUseOutAttributeStringPInvokeParametersTitle" xml:space="preserve"> | |||
<value>Do not use 'OutAttribute' on string parameters for P/Invokes</value> | |||
</data> | |||
<data name="AvoidAssemblyLocationInSingleFileDescription" xml:space="preserve"> | |||
<value>Accessing Assembly location or file path is not valid when publishing as a single-file.</value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like the word "valid" - it makes it sound like something completely wrong. Maybe just explain what it does:
"Property 'Assembly.Location' returns empty string when publishing the app as a single-file."
But I'm not a native speaker, so maybe your version actually sounds better...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually maybe we should include the fact that the assembly has to be part of the app:
"Property Assembly.Location
returns empty string for any assembly from application when published as single-file"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Saying it returns empty string is better as it's then a condition the app can check against.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case we'll probably need individual diagnostic rules for each API to describe its behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do agree that having one for each is lot of work - on the other hand it might give us opportunity to include some actionable feedback in the messages (if there's any). For example for Assembly.Location
we may point users to AppDomain.CurrentDomain.BaseDirectory
as a reasonable replacement for certain cases.
addIfNotNull(properties, tryGetSingleSymbol<IPropertySymbol>(assemblyType.GetMembers("CodeBase"))); | ||
addIfNotNull(properties, tryGetSingleSymbol<IPropertySymbol>(assemblyType.GetMembers("EscapedCodeBase"))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are now obsolete (dotnet/runtime#39261) - not sure what's the UX - will I get two warnings for the same thing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably. I didn't know we were going to obsolete them, so I can remove this.
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx
Outdated
Show resolved
Hide resolved
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx
Outdated
Show resolved
Hide resolved
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx
Show resolved
Hide resolved
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Publish/AvoidAssemblyLocationInSingleFile.cs
Outdated
Show resolved
Hide resolved
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Publish/AvoidAssemblyLocationInSingleFile.cs
Outdated
Show resolved
Hide resolved
var assemblyType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReflectionAssembly); | ||
if (assemblyType is not null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a TryGetOrCreateTypeByMetadataName
method you could use here instead.
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Publish/AvoidAssemblyLocationInSingleFile.cs
Outdated
Show resolved
Hide resolved
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Publish/AvoidAssemblyLocationInSingleFile.cs
Outdated
Show resolved
Hide resolved
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Publish/AvoidAssemblyLocationInSingleFile.cs
Outdated
Show resolved
Hide resolved
}"; | ||
return VerifyDiagnosticsAsync(src, | ||
// /0/Test0.cs(8,13): warning CA3000: Avoid `System.Reflection.Assembly.Location` when publishing as a single-file. Assemblies inside a single-file bundle do not have a file or file path. If the path to the app directory is needed, consider calling System.AppContext.BaseDirectory. | ||
VerifyCS.Diagnostic().WithSpan(8, 13, 8, 23).WithArguments("System.Reflection.Assembly.Location"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please use the markup syntax instead? It looks like {|#0:a.Location|}
(where 0 is the index) and then to test the arg you can do VerifyCS.Diagnostic.WithLocation(0).WithArgs...
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This style of diagnostics verification is what Roslyn uses for all the compiler tests. Why invent something different here? I don't see the value making the source code harder to read.
…CoreAnalyzersResources.resx Co-authored-by: Amaury Levé <evangelink@gmail.com>
…CoreAnalyzersResources.resx Co-authored-by: Amaury Levé <evangelink@gmail.com>
…dAssemblyLocationInSingleFile.cs Co-authored-by: Amaury Levé <evangelink@gmail.com>
…dAssemblyLocationInSingleFile.cs Co-authored-by: Amaury Levé <evangelink@gmail.com>
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx
Outdated
Show resolved
Hide resolved
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Publish/AvoidAssemblyLocationInSingleFile.cs
Outdated
Show resolved
Hide resolved
…CoreAnalyzersResources.resx Co-authored-by: Amaury Levé <evangelink@gmail.com>
…dAssemblyLocationInSingleFile.cs Co-authored-by: Amaury Levé <evangelink@gmail.com>
</data> | ||
<data name="AvoidAssemblyGetFilesInSingleFile" xml:space="preserve"> | ||
<value>Assemblies embedded in a single-file app cannot have additional files in the manifest.</value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it would be better to similarly to Location describe the actual behavior. In this case the API is going to throw an exception (it already does for in-memory assemblies outside of single-file).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue at dotnet/runtime#38405 seems to imply the expected behavior is to return null or empty array.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll probably have to update that issue. More recent discussions:
Assembly.CodeBase
: Assembly.CodeBase returns nonexistent path for assemblies from single-file runtime#40087 - the agreement is to actually throwPlatformNotSupported
- it's "safer" and we're obsoleting these anyway, so making them work feels contraproductive.- Module.FullyQualifiedName Module.FullyQualifiedName throws in single-file app runtime#40103 - should return
<Unknown>
. This will make it work the same as memory-loaded assemblies. This also means thatGetFile
/GetFiles
will throw (as it does in non-single-file for memory-loaded assemblies).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about AssemblyName.CodeBase
? AFAIK that's not even obsolete
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally would expect Assembly.CodeBase
and AssemblyName.CodeBase
to behave the same. There has been discussion on it, but it sort of disappeared: dotnet/runtime#31127
I'll start it again.
Is the plan to share warning codes with a future single-file analysis done by the linker? If so we should make sure to reserve |
@sbomer Yeah this was basically a set up for that exact idea. That we could move warnings back and forth between the linker and this analyzer framework by re-using error codes. I agree, it seems like a good idea to reserve those codes in the linker. Do you know how I should do that? |
I filed dotnet/linker#1393 to document this - I think it's enough for now. If we wanted to we could add a check to ensure we don't use those warning numbers. |
So I'm assuming this means 1XXX for linker and single file errors(?), 2XXX for linker warnings 3XXX for single file warnings |
@tlakollo Yup, that was the idea. |
@@ -39,6 +39,9 @@ CA2354 | Security | Disabled | DataSetDataTableInIFormatterSerializableObjectGra | |||
CA2355 | Security | Disabled | DataSetDataTableInSerializableObjectGraphAnalyzer, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca2355) | |||
CA2356 | Security | Disabled | DataSetDataTableInWebSerializableObjectGraphAnalyzer, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca2356) | |||
|
|||
IL3000 | Publish | Warning | DoNotUseAssemblyLocationInSingleFile, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/il3000) | |||
IL3001 | Publish | Warning | DoNotUseAssemblyGetFilesInSingleFile, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/il3001) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding a new range row to https://github.com/dotnet/roslyn-analyzers/blob/master/src/Utilities/Compiler/DiagnosticCategoryAndIdRanges.txt. This is an additional file which is used by a meta-analyzer on the repo that enforces few things:
- No two analyzers share the same diagnostic ID in an analyzer package in this repo
- There are no gaps introduced in the diagnostic ID range
- Diagnostic ID selected for a specific category falls in the allowed range for that category
namespace Microsoft.NetCore.Analyzers.Publish | ||
{ | ||
/// <summary> | ||
/// CA3000: Do not use Assembly.Location in single-file publish |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix ID in doc comment?
|
||
return; | ||
|
||
static bool contains<T, TComp>(List<T> list, T elem, TComp comparer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We follow the Roslyn IDE conventions in this repo for local function names
static bool contains<T, TComp>(List<T> list, T elem, TComp comparer) | |
static bool Contains<T, TComp>(List<T> list, T elem, TComp comparer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yuck :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll do it, but I'd like to register my objection as the designer and author of local functions 😄
return; | ||
} | ||
|
||
var properties = new List<IPropertySymbol>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this data structure be thread-safe given we will get concurrent callbacks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be best to use an ImmutableHashSet/ImmutableArray for these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you decide to use ImmutableHashSet, then you can use this extension method:
public static void AddIfNotNull<T>(this ImmutableHashSet<T>.Builder builder, T? item) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be safe given that threading model. ImmutableHashSet is probably a bad idea for lists of < 8 elements. I can move to ImmutableArray instead.
…dAssemblyLocationInSingleFile.cs Co-authored-by: Manish Vasani <mavasani@microsoft.com>
</data> | ||
<data name="AvoidAssemblyGetFilesInSingleFile" xml:space="preserve"> | ||
<value>'{0}' will throw for assemblies embedded in a single-file app.</value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This resource string is used as both the title and message for IL3001
. Title string should not have any string format arguments, so you probably want to use separate resource strings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I meant to re-use AvoidAssemblyLocationInSingleFileTitle, since the title actually makes sense for both. good catch.
return false; | ||
} | ||
|
||
static TSymbol? tryGetSingleSymbol<TSymbol>(ImmutableArray<ISymbol> members) where TSymbol : class, ISymbol |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static TSymbol? tryGetSingleSymbol<TSymbol>(ImmutableArray<ISymbol> members) where TSymbol : class, ISymbol | |
static TSymbol? TryGetSingleSymbol<TSymbol>(ImmutableArray<ISymbol> members) where TSymbol : class, ISymbol |
return candidate; | ||
} | ||
|
||
static void addIfNotNull<TSymbol>(List<TSymbol> properties, TSymbol? p) where TSymbol : class, ISymbol |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static void addIfNotNull<TSymbol>(List<TSymbol> properties, TSymbol? p) where TSymbol : class, ISymbol | |
static void AddIfNotNull<TSymbol>(List<TSymbol> properties, TSymbol? p) where TSymbol : class, ISymbol |
@@ -43,6 +43,9 @@ CA2361 | Security | Disabled | DoNotUseDataSetReadXml, [Documentation](https://d | |||
CA2362 | Security | Disabled | DataSetDataTableInSerializableTypeAnalyzer, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca2362) | |||
CA2363 | Security | Disabled | DataSetDataTableInSerializableTypeAnalyzer, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/ca2363) | |||
|
|||
IL3000 | Publish | Warning | DoNotUseAssemblyLocationInSingleFile, [Documentation](https://docs.microsoft.com/visualstudio/code-quality/il3000) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to create issue(s) on https://github.com/MicrosoftDocs/visualstudio-docs/issues to add documentation for the rules at https://docs.microsoft.com/visualstudio/code-quality/il3000 and https://docs.microsoft.com/visualstudio/code-quality/il3001?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall LGTM, just have some minor nit suggestions.
@mavasani You can merge this if you want, I don't have commit privileges for this repo |
Interesting, I though all roslyn team members have the permissions. Let me explicitly add you. |
@agocke Can you try again? |
@mavasani I'm not a Roslyn team member any more 😄 |
@agocke FYI, the change made to the file |
Woops, fixed here #3944 |
When publishing single-file applications, certain APIs are very likely
to return incorrect results. Most notably, asking for the Location of
any Assembly loaded from the single-file will return an empty string,
instead of a file path. To minimize suprise, this analyzer flags all
usages of the APIs most likely to break when used in single-file when
the PublishSingleFile property is set to true, and the
IncludeAllContentForSelfExtractProperty is not true (which provides the
legacy API behavior).