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

Add an analyzer for PublishSingleFile #3921

Merged
merged 13 commits into from
Aug 3, 2020
Merged

Conversation

agocke
Copy link
Member

@agocke agocke commented Jul 29, 2020

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).

@agocke
Copy link
Member Author

agocke commented Jul 29, 2020

@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
Copy link

codecov bot commented Jul 29, 2020

Codecov Report

Merging #3921 into master will decrease coverage by 0.00%.
The diff coverage is 99.18%.

@@            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>
Copy link
Member

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...

Copy link
Member

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"

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated

Copy link
Member

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.

Comment on lines 72 to 73
addIfNotNull(properties, tryGetSingleSymbol<IPropertySymbol>(assemblyType.GetMembers("CodeBase")));
addIfNotNull(properties, tryGetSingleSymbol<IPropertySymbol>(assemblyType.GetMembers("EscapedCodeBase")));
Copy link
Member

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?

Copy link
Member Author

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.

Comment on lines 67 to 68
var assemblyType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReflectionAssembly);
if (assemblyType is not null)
Copy link
Member

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.

}";
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"),
Copy link
Member

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....

Copy link
Member Author

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.

agocke and others added 4 commits July 29, 2020 14:29
…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>
agocke and others added 3 commits July 29, 2020 18:06
…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>
Copy link
Member

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).

Copy link
Member Author

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.

Copy link
Member

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:

Copy link
Member Author

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

Copy link
Member

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.

@sbomer
Copy link
Member

sbomer commented Jul 30, 2020

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 IL3000/IL3001 in mono/linker to avoid conflicts - or if not we might want to use a different prefix here.

@agocke
Copy link
Member Author

agocke commented Jul 30, 2020

@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?

@sbomer
Copy link
Member

sbomer commented Jul 30, 2020

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.

@tlakollo
Copy link

So I'm assuming this means 1XXX for linker and single file errors(?), 2XXX for linker warnings 3XXX for single file warnings

@agocke
Copy link
Member Author

agocke commented Jul 30, 2020

@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)
Copy link
Contributor

@mavasani mavasani Jul 30, 2020

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:

  1. No two analyzers share the same diagnostic ID in an analyzer package in this repo
  2. There are no gaps introduced in the diagnostic ID range
  3. Diagnostic ID selected for a specific category falls in the allowed range for that category

@agocke agocke marked this pull request as ready for review July 31, 2020 19:41
@agocke agocke requested a review from a team as a code owner July 31, 2020 19:41
namespace Microsoft.NetCore.Analyzers.Publish
{
/// <summary>
/// CA3000: Do not use Assembly.Location in single-file publish
Copy link
Contributor

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)
Copy link
Contributor

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

Suggested change
static bool contains<T, TComp>(List<T> list, T elem, TComp comparer)
static bool Contains<T, TComp>(List<T> list, T elem, TComp comparer)

Copy link
Member Author

Choose a reason for hiding this comment

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

yuck :)

Copy link
Member Author

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>();
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor

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)

Copy link
Member Author

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>
Copy link
Contributor

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.

Copy link
Member Author

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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)
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

@mavasani mavasani left a 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.

@agocke
Copy link
Member Author

agocke commented Aug 3, 2020

@mavasani You can merge this if you want, I don't have commit privileges for this repo

@mavasani
Copy link
Contributor

mavasani commented Aug 3, 2020

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.

@mavasani
Copy link
Contributor

mavasani commented Aug 3, 2020

@agocke Can you try again?

@agocke
Copy link
Member Author

agocke commented Aug 3, 2020

@mavasani I'm not a Roslyn team member any more 😄

@agocke agocke merged commit 0c78e1e into dotnet:master Aug 3, 2020
@agocke agocke deleted the single-file branch August 3, 2020 19:13
@carlossanlop
Copy link
Member

@agocke FYI, the change made to the file src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md added a space before the two new table rows, and markdown is not including them in the table:

https://github.com/dotnet/roslyn-analyzers/blob/master/src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md

@agocke
Copy link
Member Author

agocke commented Aug 4, 2020

Woops, fixed here #3944

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants