Skip to content

Commit

Permalink
MA0015 supports primary constructors
Browse files Browse the repository at this point in the history
  • Loading branch information
meziantou committed Jul 12, 2023
1 parent ba0aad1 commit 9b254d6
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 243 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
- run: dotnet build src/Meziantou.Analyzer/Meziantou.Analyzer.csproj --configuration Release /p:RoslynVersion=roslyn3.8 /p:Version=${{ needs.compute_package_version.outputs.package_version }}
- run: dotnet build src/Meziantou.Analyzer/Meziantou.Analyzer.csproj --configuration Release /p:RoslynVersion=roslyn4.2 /p:Version=${{ needs.compute_package_version.outputs.package_version }}
- run: dotnet build src/Meziantou.Analyzer/Meziantou.Analyzer.csproj --configuration Release /p:RoslynVersion=roslyn4.4 /p:Version=${{ needs.compute_package_version.outputs.package_version }}
- run: dotnet build src/Meziantou.Analyzer/Meziantou.Analyzer.csproj --configuration Release /p:RoslynVersion=roslyn4.6 /p:Version=${{ needs.compute_package_version.outputs.package_version }}

- run: dotnet restore src/Meziantou.Analyzer.pack.csproj
- run: dotnet pack src/Meziantou.Analyzer.pack.csproj --configuration Release --no-build /p:Version=${{ needs.compute_package_version.outputs.package_version }}
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Meziantou.Polyfill" Version="1.0.11">
<PackageReference Include="Meziantou.Polyfill" Version="1.0.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
Expand Down
14 changes: 13 additions & 1 deletion Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,26 @@
</PropertyGroup>
</When>

<When Condition="$(RoslynVersion) == 'roslyn4.6'">
<ItemGroup>
<PackageReference Update="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" />
<PackageReference Update="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.6.0" />
</ItemGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants);ROSLYN_4_6;ROSLYN_4_2_OR_GREATER;ROSLYN_4_4_OR_GREATER;ROSLYN_4_6_OR_GREATER</DefineConstants>
<DefineConstants>$(DefineConstants);CSHARP9_OR_GREATER;CSHARP10_OR_GREATER;CSHARP11_OR_GREATER</DefineConstants>
<NoWarn>$(NoWarn);nullable</NoWarn>
</PropertyGroup>
</When>

<Otherwise>
<ItemGroup>
<PackageReference Update="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageReference Update="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.6.0" />
</ItemGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants);ROSLYN4_4;ROSLYN_4_2_OR_GREATER;ROSLYN_4_4_OR_GREATER;ROSLYN_4_5_OR_GREATER;ROSLYN_4_6_OR_GREATER</DefineConstants>
<DefineConstants>$(DefineConstants);CSHARP9_OR_GREATER;CSHARP10_OR_GREATER;CSHARP11_OR_GREATER</DefineConstants>
<DefineConstants>$(DefineConstants);CSHARP9_OR_GREATER;CSHARP10_OR_GREATER;CSHARP11_OR_GREATER;CSHARP12_OR_GREATER</DefineConstants>
<NoWarn>$(NoWarn);CS0618</NoWarn>
</PropertyGroup>
</Otherwise>
Expand Down
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer.pack.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
<None Include="$(MSBuildThisFileDirectory)\Meziantou.Analyzer\bin\roslyn3.8\$(Configuration)\netstandard2.0\Meziantou.Analyzer.dll" Pack="true" PackagePath="analyzers/dotnet/roslyn3.8/cs/" />
<None Include="$(MSBuildThisFileDirectory)\Meziantou.Analyzer\bin\roslyn4.2\$(Configuration)\netstandard2.0\Meziantou.Analyzer.dll" Pack="true" PackagePath="analyzers/dotnet/roslyn4.2/cs/" />
<None Include="$(MSBuildThisFileDirectory)\Meziantou.Analyzer\bin\roslyn4.4\$(Configuration)\netstandard2.0\Meziantou.Analyzer.dll" Pack="true" PackagePath="analyzers/dotnet/roslyn4.4/cs/" />
<None Include="$(MSBuildThisFileDirectory)\Meziantou.Analyzer\bin\roslyn4.6\$(Configuration)\netstandard2.0\Meziantou.Analyzer.dll" Pack="true" PackagePath="analyzers/dotnet/roslyn4.6/cs/" />
</ItemGroup>
</Project>
110 changes: 109 additions & 1 deletion src/Meziantou.Analyzer/Internals/OperationExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
Expand Down Expand Up @@ -98,7 +99,7 @@ public static IOperation UnwrapImplicitConversionOperations(this IOperation oper

return operation;
}

public static IOperation UnwrapConversionOperations(this IOperation operation)
{
if (operation is IConversionOperation conversionOperation)
Expand Down Expand Up @@ -133,4 +134,111 @@ public static bool HasArgumentOfType(this IInvocationOperation operation, ITypeS

return null;
}

public static bool IsInStaticContext(this IOperation operation, CancellationToken cancellationToken) => IsInStaticContext(operation, cancellationToken, out _);
public static bool IsInStaticContext(this IOperation operation, CancellationToken cancellationToken, out int parentStaticMemberStartPosition)
{
// Local functions can be nested, and an instance local function can be declared
// in a static local function. So, you need to continue to check ancestors when a
// local function is not static.
foreach (var member in operation.Syntax.Ancestors())
{
if (member is LocalFunctionStatementSyntax localFunction)
{
var symbol = operation.SemanticModel!.GetDeclaredSymbol(localFunction, cancellationToken);
if (symbol != null && symbol.IsStatic)
{
parentStaticMemberStartPosition = localFunction.GetLocation().SourceSpan.Start;
return true;
}
}
else if (member is LambdaExpressionSyntax lambdaExpression)
{
var symbol = operation.SemanticModel!.GetSymbolInfo(lambdaExpression, cancellationToken).Symbol;
if (symbol != null && symbol.IsStatic)
{
parentStaticMemberStartPosition = lambdaExpression.GetLocation().SourceSpan.Start;
return true;
}
}
else if (member is AnonymousMethodExpressionSyntax anonymousMethod)
{
var symbol = operation.SemanticModel!.GetSymbolInfo(anonymousMethod, cancellationToken).Symbol;
if (symbol != null && symbol.IsStatic)
{
parentStaticMemberStartPosition = anonymousMethod.GetLocation().SourceSpan.Start;
return true;
}
}
else if (member is MethodDeclarationSyntax methodDeclaration)
{
parentStaticMemberStartPosition = methodDeclaration.GetLocation().SourceSpan.Start;

var symbol = operation.SemanticModel!.GetDeclaredSymbol(methodDeclaration, cancellationToken);
return symbol != null && symbol.IsStatic;
}
}

parentStaticMemberStartPosition = -1;
return false;
}

public static IEnumerable<ISymbol> LookupAvailableSymbols(this IOperation operation, CancellationToken cancellationToken)
{
// Find available symbols
var operationLocation = operation.Syntax.GetLocation().SourceSpan.Start;
var isInStaticContext = operation.IsInStaticContext(cancellationToken, out var parentStaticMemberStartPosition);
foreach (var symbol in operation.SemanticModel!.LookupSymbols(operationLocation))
{
// LookupSymbols check the accessibility of the symbol, but it can
// suggest instance members when the current context is static.
if (symbol is IFieldSymbol field && isInStaticContext && !field.IsStatic)
continue;

if (symbol is IPropertySymbol { GetMethod: not null } property && isInStaticContext && !property.IsStatic)
continue;

// Locals can be returned even if there are not valid in the current context. For instance,
// it can return locals declared after the current location. Or it can return locals that
// should not be accessible in a static local function.
//
// void Sample()
// {
// int local = 0;
// static void LocalFunction() => local; <-- local is invalid here but LookupSymbols suggests it
// }
//
// Parameters from the ancestor methods are also returned even if the operation is in a static local function.
if (symbol.Kind is SymbolKind.Local or SymbolKind.Parameter)
{
var isValid = true;
foreach (var location in symbol.Locations)
{
isValid &= IsValid(location, operationLocation, isInStaticContext ? parentStaticMemberStartPosition : null);
if (!isValid)
break;
}

if (!isValid)
continue;

static bool IsValid(Location location, int operationLocation, int? staticContextStart)
{
var localPosition = location.SourceSpan.Start;

// The local is declared after the current expression
if (localPosition > operationLocation)
return false;

// The local is declared outside the static local function
if (staticContextStart.HasValue && localPosition < staticContextStart.GetValueOrDefault())
return false;

return true;
}
}

yield return symbol;
}
}
}
12 changes: 12 additions & 0 deletions src/Meziantou.Analyzer/Internals/SymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,16 @@ public static bool IsTopLevelStatementsEntryPointType([NotNullWhen(true)] this I

return false;
}

public static ITypeSymbol? GetSymbolType(this ISymbol symbol)
{
return symbol switch
{
IParameterSymbol parameter => parameter.Type,
IFieldSymbol field => field.Type,
IPropertySymbol { GetMethod: not null } property => property.Type,
ILocalSymbol local => local.Type,
_ => null,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,128 +120,15 @@ private static void Analyze(OperationAnalysisContext context)

private static IEnumerable<string> GetParameterNames(IOperation operation, CancellationToken cancellationToken)
{
var semanticModel = operation.SemanticModel!;
var node = operation.Syntax;
while (node != null)
var symbols = operation.LookupAvailableSymbols(cancellationToken);
foreach (var symbol in symbols)
{
switch (node)
switch (symbol)
{
case AccessorDeclarationSyntax accessor:
if (accessor.IsKind(SyntaxKind.SetAccessorDeclaration))
{
yield return "value";
}

case IParameterSymbol parameterSymbol:
yield return parameterSymbol.Name;
break;

case PropertyDeclarationSyntax _:
yield break;

case IndexerDeclarationSyntax indexerDeclarationSyntax:
{
var symbol = semanticModel.GetDeclaredSymbol(indexerDeclarationSyntax, cancellationToken);
if (symbol != null)
{
foreach (var parameter in symbol.Parameters)
yield return parameter.Name;
}

yield break;
}

case MethodDeclarationSyntax methodDeclaration:
{
var symbol = semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken);
if (symbol != null)
{
foreach (var parameter in symbol.Parameters)
yield return parameter.Name;
}

yield break;
}

case LocalFunctionStatementSyntax localFunctionStatement:
{
if (semanticModel.GetDeclaredSymbol(localFunctionStatement, cancellationToken) is IMethodSymbol symbol)
{
foreach (var parameter in symbol.Parameters)
yield return parameter.Name;
}

break;
}

case ConstructorDeclarationSyntax constructorDeclaration:
{
var symbol = semanticModel.GetDeclaredSymbol(constructorDeclaration, cancellationToken);
if (symbol != null)
{
foreach (var parameter in symbol.Parameters)
yield return parameter.Name;
}

yield break;
}

case OperatorDeclarationSyntax operatorDeclaration:
{
var symbol = semanticModel.GetDeclaredSymbol(operatorDeclaration, cancellationToken);
if (symbol != null)
{
foreach (var parameter in symbol.Parameters)
yield return parameter.Name;
}

yield break;
}

case ParenthesizedLambdaExpressionSyntax parenthesizedLambdaExpressionSyntax:
{
foreach (var parameter in parenthesizedLambdaExpressionSyntax.ParameterList.Parameters)
{
if (!string.IsNullOrEmpty(parameter.Identifier.ValueText))
yield return parameter.Identifier.ValueText;
}

if (parenthesizedLambdaExpressionSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)))
yield break;

break;
}

case SimpleLambdaExpressionSyntax lambdaExpressionSyntax:
{
if (!string.IsNullOrEmpty(lambdaExpressionSyntax.Parameter?.Identifier.ValueText))
{
yield return lambdaExpressionSyntax.Parameter.Identifier.ValueText;
}

if (lambdaExpressionSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)))
yield break;

break;
}

case AnonymousMethodExpressionSyntax anonymousMethodExpressionSyntax:
{
if (anonymousMethodExpressionSyntax.ParameterList != null)
{
foreach (var parameter in anonymousMethodExpressionSyntax.ParameterList.Parameters)
{
if (!string.IsNullOrEmpty(parameter.Identifier.ValueText))
yield return parameter.Identifier.ValueText;
}
}

if (anonymousMethodExpressionSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword)))
yield break;

break;
}
}

node = node.Parent;
}
}
}
Loading

0 comments on commit 9b254d6

Please sign in to comment.