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

Redo pubternalyzer with Roslyn Operations #21996

Merged
merged 13 commits into from
Aug 16, 2020
2 changes: 1 addition & 1 deletion benchmark/EFCore.Benchmarks/EFCore.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<ItemGroup Condition=" '$(Configuration)' == 'Release' Or '$(Configuration)' == 'Debug' ">
<ProjectReference Include="..\..\src\EFCore.Relational\EFCore.Relational.csproj" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.3.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisVersion)" />
</ItemGroup>

<ItemGroup Condition=" '$(Configuration)' == 'Release22' Or '$(Configuration)' == 'Debug22' ">
Expand Down
3 changes: 3 additions & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@
<MicrosoftExtensionsHostFactoryResolverSourcesVersion>5.0.0-rc.1.20402.3</MicrosoftExtensionsHostFactoryResolverSourcesVersion>
<MicrosoftExtensionsLoggingVersion>5.0.0-rc.1.20402.3</MicrosoftExtensionsLoggingVersion>
</PropertyGroup>
<PropertyGroup Label="Other dependencies">
<MicrosoftCodeAnalysisVersion>3.7.0</MicrosoftCodeAnalysisVersion>
</PropertyGroup>
</Project>
18 changes: 18 additions & 0 deletions src/EFCore.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## Release 2.1.0

### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
EF1000 | Usage | Warning | RawSqlStringInjectionDiagnosticAnalyzer, [Documentation](https://docs.microsoft.com/ef/core/querying/raw-sql)

## Release 3.0.0

### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
EF1001 | Usage | Warning | InternalUsageDiagnosticAnalyzer

### Removed Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
EF1000 | Security | Disabled | RawSqlStringInjectionDiagnosticAnalyzer, [Documentation](https://docs.microsoft.com/ef/core/querying/raw-sql)
Empty file.
8 changes: 7 additions & 1 deletion src/EFCore.Analyzers/EFCore.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@

<ItemGroup>
<!-- NB: Version affects the minimum required Visual Studio version -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.3.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisVersion)" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic" Version="$(MicrosoftCodeAnalysisVersion)" PrivateAssets="all" />
<PackageReference Update="NETStandard.Library" PrivateAssets="all" />
</ItemGroup>

Expand All @@ -32,4 +33,9 @@
</ItemGroup>
</Target>

<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>

</Project>
282 changes: 222 additions & 60 deletions src/EFCore.Analyzers/InternalUsageDiagnosticAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

using CSharpSyntax = Microsoft.CodeAnalysis.CSharp.Syntax;
using VBSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax;

namespace Microsoft.EntityFrameworkCore
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public class InternalUsageDiagnosticAnalyzer : DiagnosticAnalyzer
{
public const string Id = "EF1001";

public const string MessageFormat
= "{0} is an internal API that supports the Entity Framework Core infrastructure and " +
"not subject to the same compatibility standards as public APIs. " +
"It may be changed or removed without notice in any release.";
= "{0} is an internal API that supports the Entity Framework Core infrastructure and "
+ "not subject to the same compatibility standards as public APIs. "
+ "It may be changed or removed without notice in any release.";

protected const string DefaultTitle = "Internal EF Core API usage.";
protected const string Category = "Usage";
Expand All @@ -40,84 +42,244 @@ private static readonly DiagnosticDescriptor _descriptor
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(
AnalyzeNode,
SyntaxKind.SimpleMemberAccessExpression,
SyntaxKind.ObjectCreationExpression,
SyntaxKind.ClassDeclaration);
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterOperationAction(AnalyzeNode,
OperationKind.FieldReference,
OperationKind.PropertyReference,
OperationKind.MethodReference,
OperationKind.EventReference,
OperationKind.Invocation,
OperationKind.ObjectCreation,
OperationKind.VariableDeclaration,
OperationKind.TypeOf);

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType, SymbolKind.Property, SymbolKind.Field, SymbolKind.Event);
roji marked this conversation as resolved.
Show resolved Hide resolved
}

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
private static void AnalyzeNode(OperationAnalysisContext context)
{
switch (context.Node)
switch (context.Operation.Kind)
{
case MemberAccessExpressionSyntax memberAccessSyntax:
{
if (context.SemanticModel.GetSymbolInfo(context.Node, context.CancellationToken).Symbol is ISymbol symbol
&& !Equals(symbol.ContainingAssembly, context.Compilation.Assembly))
case OperationKind.FieldReference:
AnalyzeMember(context, ((IFieldReferenceOperation)context.Operation).Field);
break;
case OperationKind.PropertyReference:
AnalyzeMember(context, ((IPropertyReferenceOperation)context.Operation).Property);
break;
case OperationKind.EventReference:
AnalyzeMember(context, ((IEventReferenceOperation)context.Operation).Event);
break;
case OperationKind.MethodReference:
AnalyzeMember(context, ((IMethodReferenceOperation)context.Operation).Method);
break;
case OperationKind.ObjectCreation:
AnalyzeMember(context, ((IObjectCreationOperation)context.Operation).Constructor);
break;
case OperationKind.Invocation:
AnalyzeInvocation(context, (IInvocationOperation)context.Operation);
break;
case OperationKind.VariableDeclaration:
AnalyzeVariableDeclaration(context, ((IVariableDeclarationOperation)context.Operation));
break;
case OperationKind.TypeOf:
AnalyzeTypeof(context, ((ITypeOfOperation)context.Operation));
break;
default:
throw new ArgumentException($"Unexpected {nameof(OperationKind)}: {context.Operation.Kind}");
}

}

private static void AnalyzeMember(OperationAnalysisContext context, ISymbol symbol)
{
if ((object)symbol.ContainingAssembly == context.Compilation.Assembly)
{
// Skip all methods inside the same assembly - internal access is fine
return;
}

var containingType = symbol.ContainingType;

switch (symbol)
{
case IMethodSymbol _:
case IFieldSymbol _:
case IPropertySymbol _:
case IEventSymbol _:
roji marked this conversation as resolved.
Show resolved Hide resolved
if (HasInternalAttribute(symbol))
{
var containingType = symbol.ContainingType;

if (HasInternalAttribute(symbol))
{
context.ReportDiagnostic(
Diagnostic.Create(_descriptor, memberAccessSyntax.Name.GetLocation(), $"{containingType}.{symbol.Name}"));
return;
}

if (IsInInternalNamespace(containingType)
|| HasInternalAttribute(containingType))
{
context.ReportDiagnostic(Diagnostic.Create(_descriptor, memberAccessSyntax.Name.GetLocation(), containingType));
return;
}
ReportDiagnostic(context, symbol.Name == ".ctor" ? (object)containingType : $"{containingType}.{symbol.Name}");
return;
}
break;
}

if (IsInternal(context, containingType))
roji marked this conversation as resolved.
Show resolved Hide resolved
{
ReportDiagnostic(context, containingType);
}
}

private static void AnalyzeInvocation(OperationAnalysisContext context, IInvocationOperation invocation)
{
// First check for any internal type parameters
foreach (var a in invocation.TargetMethod.TypeArguments)

Choose a reason for hiding this comment

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

Do we do similar analysis for generic type arguments of named types?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not at the moment. Do you have guidance on how to get the generic type arguments of an ITypeSymbol?

Copy link
Member Author

Choose a reason for hiding this comment

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

Scoping out to #22086, any guidance on this would be appreciated.

{
if (IsInternal(context, a))
{
context.ReportDiagnostic(Diagnostic.Create(_descriptor, context.Operation.Syntax.GetLocation(), a));
}
}

// Then check the method being invoked
AnalyzeMember(context, invocation.TargetMethod);
}

private static void AnalyzeVariableDeclaration(OperationAnalysisContext context, IVariableDeclarationOperation variableDeclaration)
{
foreach (var declarator in variableDeclaration.Declarators)
{
if (IsInternal(context, declarator.Symbol.Type))
{
var syntax = context.Operation.Syntax switch
{
CSharpSyntax.VariableDeclarationSyntax s => s.Type,
_ => context.Operation.Syntax
};
context.ReportDiagnostic(Diagnostic.Create(_descriptor, syntax.GetLocation(), declarator.Symbol.Type));
return;
}
}
}

private static void AnalyzeTypeof(OperationAnalysisContext context, ITypeOfOperation typeOf)
{
if (IsInternal(context, typeOf.TypeOperand))
{
ReportDiagnostic(context, typeOf.TypeOperand);
}
}

private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
switch (context.Symbol)
{
case INamedTypeSymbol symbol:
AnalyzeNamedTypeSymbol(context, symbol);
break;

case IFieldSymbol symbol:
AnalyzeMemberDeclarationTypeSymbol(context, symbol, symbol.Type);
break;

case IPropertySymbol symbol:
AnalyzeMemberDeclarationTypeSymbol(context, symbol, symbol.Type);
break;

case IEventSymbol symbol:
AnalyzeMemberDeclarationTypeSymbol(context, symbol, symbol.Type);
break;

case ObjectCreationExpressionSyntax creationSyntax:
default:
throw new ArgumentException($"Unexpected {nameof(ISymbol)}: {context.Symbol.GetType().Name}");
}
}

private static void AnalyzeNamedTypeSymbol(SymbolAnalysisContext context, INamedTypeSymbol symbol)
{
if (symbol.BaseType is ITypeSymbol baseSymbol
&& IsInternal(context, baseSymbol))
{
foreach (var declaringSyntax in symbol.DeclaringSyntaxReferences)
roji marked this conversation as resolved.
Show resolved Hide resolved
{
if (context.SemanticModel.GetSymbolInfo(context.Node, context.CancellationToken).Symbol is ISymbol symbol
&& !Equals(symbol.ContainingAssembly, context.Compilation.Assembly))
var location = declaringSyntax.GetSyntax() switch
{
var containingType = symbol.ContainingType;

if (HasInternalAttribute(symbol))
{
context.ReportDiagnostic(Diagnostic.Create(_descriptor, creationSyntax.GetLocation(), containingType));
return;
}

if (IsInInternalNamespace(containingType)
|| HasInternalAttribute(containingType))
{
context.ReportDiagnostic(Diagnostic.Create(_descriptor, creationSyntax.Type.GetLocation(), containingType));
return;
}
}
CSharpSyntax.ClassDeclarationSyntax s when s.BaseList?.Types.Count > 0
=> s.BaseList.Types[0].GetLocation(),
{ } otherSyntax => otherSyntax.GetLocation()
};

return;
context.ReportDiagnostic(Diagnostic.Create(_descriptor, location, baseSymbol));
}
}

case ClassDeclarationSyntax declarationSyntax:
foreach (var iface in symbol.Interfaces.Where(i => IsInternal(context, i)))
{
foreach (var declaringSyntax in symbol.DeclaringSyntaxReferences)
{
if (context.SemanticModel.GetDeclaredSymbol(declarationSyntax)?.BaseType is ISymbol symbol
&& !Equals(symbol.ContainingAssembly, context.Compilation.Assembly)
&& (IsInInternalNamespace(symbol) || HasInternalAttribute(symbol))
&& declarationSyntax.BaseList?.Types.Count > 0)
var location = declaringSyntax.GetSyntax() switch
{
context.ReportDiagnostic(Diagnostic.Create(_descriptor, declarationSyntax.BaseList.Types[0].GetLocation(), symbol));
}
CSharpSyntax.ClassDeclarationSyntax s => s.Identifier.GetLocation(),
{ } otherSyntax => otherSyntax.GetLocation()
};

return;
context.ReportDiagnostic(Diagnostic.Create(_descriptor, location, iface));
}
}
}

private static void AnalyzeMemberDeclarationTypeSymbol(
SymbolAnalysisContext context,
ISymbol declarationSymbol,
ITypeSymbol typeSymbol)
{
if (IsInternal(context, typeSymbol))
{
foreach (var declaringSyntax in declarationSymbol.DeclaringSyntaxReferences)
{
ReportDiagnostic(context, declaringSyntax.GetSyntax(), typeSymbol);
}
}
}

private static void ReportDiagnostic(OperationAnalysisContext context, object messageArg)
=> context.ReportDiagnostic(Diagnostic.Create(_descriptor, NarrowDownSyntax(context.Operation.Syntax).GetLocation(), messageArg));

private static void ReportDiagnostic(SymbolAnalysisContext context, SyntaxNode syntax, object messageArg)
=> context.ReportDiagnostic(Diagnostic.Create(_descriptor, NarrowDownSyntax(syntax).GetLocation(), messageArg));

/// <summary>
/// Given a syntax node, pattern matches some known types and returns a narrowed-down node for the type syntax which
/// should be reported in diagnostics.
/// </summary>
private static SyntaxNode NarrowDownSyntax(SyntaxNode syntax)
=> syntax switch
{
CSharpSyntax.InvocationExpressionSyntax s
when s.Expression is CSharpSyntax.MemberAccessExpressionSyntax memberAccessSyntax
=> memberAccessSyntax.Name,
CSharpSyntax.MemberAccessExpressionSyntax s => s.Name,
CSharpSyntax.ObjectCreationExpressionSyntax s => s.Type,
CSharpSyntax.PropertyDeclarationSyntax s => s.Type,
CSharpSyntax.VariableDeclaratorSyntax declarator
=> declarator.Parent is CSharpSyntax.VariableDeclarationSyntax declaration
? declaration.Type
: (SyntaxNode)declarator,
CSharpSyntax.TypeOfExpressionSyntax s => s.Type,

VBSyntax.InvocationExpressionSyntax s
when s.Expression is VBSyntax.MemberAccessExpressionSyntax memberAccessSyntax
=> memberAccessSyntax.Name,
VBSyntax.MemberAccessExpressionSyntax s => s.Name,
VBSyntax.ObjectCreationExpressionSyntax s => s.Type,
VBSyntax.TypeOfExpressionSyntax s => s.Type,
Comment on lines +287 to +304

Choose a reason for hiding this comment

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

It seems it would be better if you have different language specific analyzer assemblies for language specific code. Otherwise, your analyzer will cause a C# only solution to load all VB Roslyn assemblies and vice versa, which has an unnecessary assembly load overhead.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a good point - even if the assembly load overhead would only apply at build time...

The problem is that our analyzers package - Microsoft.EntityFrameworkCore.Analyzers - isn't typically referenced directly by user, but is brought in transitively as a dependency of Microsoft.EntityFrameworkCore. I know that ASP.NET also have a similar pattern, do you have any guidance on this, or any particular reason why the additional VB assembly load should be avoided?

If this is a big issue, we can drop the VB dependency altogether - it's only used to produce better/narrower diagnostic locations - the warnings themselves would still appear as we're using language-independent APIs here.

Choose a reason for hiding this comment

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

Normally the way this would be done is that your analyzer package will have 3 assemblies - Core, C# specific and VB specific. Then the MSBuild/SDK machinery will ensure that C# projects have analyzer references to first two and VB projects to the first and third. This way if someone has a C# only solution or a VB only solution, they do not get penalized by loading non-required language Roslyn assemblies (which can be quite a few as most of Roslyn layers are divided the same way). @jmarolf can probably help with setting up/guiding on the SDK pieces.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh nice, was not aware of this... Any pointer to an analyzer package which does this would be sufficient!

Choose a reason for hiding this comment

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

@jmarolf did this work for the Microsoft.CodeAnalysis.NetAnalyzers nuget package that we now insert into the .NET SDK. He can give pointers on how you can match his apporoach.

Copy link
Member Author

Choose a reason for hiding this comment

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

Scoping this out to #22085

Copy link
Member

Choose a reason for hiding this comment

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

Otherwise, your analyzer will cause a C# only solution to load all VB Roslyn assemblies and vice versa, which has an unnecessary assembly load overhead.

Note that it's slightly worse than this implies. Csc does not provide the VB assemblies, and vbc does not provide the C# assemblies. Command line use of an analyzer that references both will work if and only if the shared compiler server VBCSCompiler.exe is used, but that will occasionally fail to process a request and silently fall back to the language-specific executables, at which point the analyzers will silently fail to run.


_ => syntax
};

private static bool IsInternal(SymbolAnalysisContext context, ITypeSymbol symbol)
=> (object)symbol.ContainingAssembly != context.Compilation.Assembly
&& (IsInInternalNamespace(symbol) || HasInternalAttribute(symbol));

private static bool IsInternal(OperationAnalysisContext context, ITypeSymbol symbol)
=> (object)symbol.ContainingAssembly != context.Compilation.Assembly
&& (IsInInternalNamespace(symbol) || HasInternalAttribute(symbol));

private static bool HasInternalAttribute(ISymbol symbol)
=> symbol != null && symbol.GetAttributes().Any(a => a.AttributeClass.Name == "EntityFrameworkInternalAttribute");
=> symbol != null
&& symbol.GetAttributes().Any(a =>
a.AttributeClass.ToDisplayString() == "Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkInternalAttribute");

private static bool IsInInternalNamespace(ISymbol symbol)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,12 @@ private void AddInclude(
var includeMethod = navigation.IsCollection ? _includeCollectionMethodInfo : _includeReferenceMethodInfo;
var includingClrType = navigation.DeclaringEntityType.ClrType;
var relatedEntityClrType = navigation.TargetEntityType.ClrType;
#pragma warning disable EF1001 // Internal EF Core API usage.
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 think you already know about these internal usages @AndriySvyryd, but just in case.

var entityEntryVariable = _trackQueryResults
? shaperBlock.Variables.Single(v => v.Type == typeof(InternalEntityEntry))
: (Expression)Expression.Constant(null, typeof(InternalEntityEntry));
#pragma warning restore EF1001 // Internal EF Core API usage.

var concreteEntityTypeVariable = shaperBlock.Variables.Single(v => v.Type == typeof(IEntityType));
var inverseNavigation = navigation.Inverse;
var fixup = GenerateFixup(
Expand Down
Loading