Skip to content

[DAM analyzer] Check for unexpected analyzer warnings in linker tests #2523

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

Merged
merged 23 commits into from
Feb 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
89db6a1
Add test run to check for unexpected analyzer warnings
sbomer Jan 19, 2022
412bc00
Allow more flexible test file organization
sbomer Jan 20, 2022
06aeeba
Check expected warning locations
sbomer Jan 20, 2022
d727951
Respect ExpectedNoWarningsAttribute
sbomer Jan 21, 2022
f723d31
Fix build
sbomer Jan 27, 2022
a0d79f5
Fix inconsistencies in Requires attribute checks (#2553)
sbomer Jan 31, 2022
574dd2b
Write generated code to disk
sbomer Jan 31, 2022
9edcc03
Merge remote-tracking branch 'origin/feature/damAnalyzer' into noExtr…
sbomer Feb 1, 2022
93c3f7e
Merge remote-tracking branch 'origin/feature/damAnalyzer' into noExtr…
sbomer Feb 1, 2022
274e9da
Add generated sources
sbomer Feb 1, 2022
50021c6
Add job displayName
sbomer Feb 1, 2022
61b8d75
Try different property syntax
sbomer Feb 1, 2022
1400962
Try removing comment syntax
sbomer Feb 1, 2022
444257c
Don't produce warnings for typeof(Foo<>)
sbomer Feb 1, 2022
f662785
Don't crash on InvalidOperation
sbomer Feb 1, 2022
f31842e
Compare symbols correctly
sbomer Feb 1, 2022
e3b2cb4
Don't run analyzer on .il test dependencies
sbomer Feb 1, 2022
cc5ab84
Mak LinkAttributesTests partial
sbomer Feb 2, 2022
de48f49
Merge remote-tracking branch 'origin/feature/damAnalyzer' into noExtr…
sbomer Feb 2, 2022
2a0cbf9
Fix test infra bugs with LogContains attributes
sbomer Feb 2, 2022
73218c8
Skip failing tests
sbomer Feb 2, 2022
094a43e
Fix remaining test infrastructure issues
sbomer Feb 2, 2022
8946e50
Run generator for all analyzer test runs
sbomer Feb 3, 2022
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
4 changes: 4 additions & 0 deletions src/ILLink.RoslynAnalyzer/DataFlow/LocalDataFlowVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ public override TValue VisitSimpleAssignment (ISimpleAssignmentOperation operati
case IDiscardOperation:
// Assignments like "_ = SomeMethod();" don't need dataflow tracking.
break;
case IInvalidOperation:
// This can happen for a field assignment in an attribute instance.
// TODO: validate against the field attributes.
break;
default:
throw new NotImplementedException (operation.Target.GetType ().ToString ());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ static void ProcessGenericParameters (SyntaxNodeAnalysisContext context)
Debug.Assert (typeParams.Length == typeArgs.Length);

for (int i = 0; i < typeParams.Length; i++) {
// Syntax like typeof (Foo<>) will have an ErrorType as the type argument.
// These uninstantiated generics should not produce warnings.
if (typeArgs[i].Kind == SymbolKind.ErrorType)
continue;
var sourceValue = GetTypeValueNodeFromGenericArgument (context, typeArgs[i]);
var targetValue = new GenericParameterValue (typeParams[i]);
foreach (var diagnostic in GetDynamicallyAccessedMembersDiagnostics (sourceValue, targetValue, context.Node.GetLocation ()))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
Expand All @@ -23,9 +23,7 @@ public readonly record struct TrimAnalysisMethodCallPattern (
public TrimAnalysisMethodCallPattern Merge (ValueSetLattice<SingleValue> lattice, TrimAnalysisMethodCallPattern other)
{
Debug.Assert (Operation == other.Operation);
#pragma warning disable RS1024 // Compare symbols correctly
Debug.Assert (CalledMethod == other.CalledMethod);
#pragma warning restore RS1024 // Compare symbols correctly
Debug.Assert (SymbolEqualityComparer.Default.Equals (CalledMethod, other.CalledMethod));
Debug.Assert (Arguments.Length == other.Arguments.Length);

var argumentsBuilder = ImmutableArray.CreateBuilder<MultiValue> ();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisVersion)" />
</ItemGroup>

</Project>
208 changes: 208 additions & 0 deletions test/ILLink.RoslynAnalyzer.Tests.Generator/TestCaseGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection.Metadata;

namespace ILLink.RoslynAnalyzer.Tests
{
public readonly struct TestCases
{
// Maps from suite name to a set of testcase names.
// Suite name is:
// - The namespace of the test class, minus "Mono.Linker.Tests.Cases", for linker tests
// - The namespace + test class name, minus "ILLink.RoslynAnalyzer.Tests", for analyzer tests
// Testcase name is:
// - The test class name, for linker tests
// - The test fact method name, minus "Tests", for analyzer tests
// For example:
// | Testcase | Suite name | Linker | Analyzer |
// |-------------------------+----------------------+--------------------------------------------+-----------------------------------------|
// | RequiresCapability | RequiresCapability | RequiresCapability class in namespcae | RequiresCapability method in class |
// | | | Mono.Linker.Tests.Cases.RequiresCapability | RequiresCapabilityTests in namespace |
// | | | | ILLink.RoslynAnalyzer.Tests |
// |-------------------------+----------------------+--------------------------------------------+-----------------------------------------|
// | RequiresOnAttributeCtor | RequiresCapability | RequiresOnAttributeCtor class in namespace | RequiresOnAttributeCtor method in class |
// | | | Mono.Linker.Tests.Cases.RequiresCapability | RequiresCapabilityTests in namespace |
// | | | | ILlink.RoslynAnalyzer.Tests |
// |-------------------------+----------------------+--------------------------------------------+-----------------------------------------|
// | UnusedPInvoke | Interop.PInvokeTests | UnusedPInvoke class in namespace | UnusedPInvoke method in class |
// | | | Mono.Linker.Tests.Cases.Interop.PInvoke | PinvokeTests in namespace |
// | | | | ILLink.RoslynAnalyzer.Tests.Interop |
public readonly Dictionary<string, HashSet<string>> Suites = new ();

public void Add (string suiteName, string name)
{
if (!Suites.TryGetValue (suiteName, out var testCases)) {
testCases = new HashSet<string> ();
Suites.Add (suiteName, testCases);
}

testCases.Add (name);
}
}

public static class TestClassGenerator
{
public const string TestNamespace = "ILLink.RoslynAnalyzer.Tests";

public static string GenerateClassHeader (string suiteName, bool newTestSuite) {
int idx = suiteName.LastIndexOf ('.');
// Test suite class from innermost namespace part
var suiteClassName = suiteName.Substring (idx + 1);
// Namespace from outer namespaces, or empty
var suiteNamespacePart = suiteClassName.Length < suiteName.Length ?
$".{suiteName.Substring (0, idx)}" : string.Empty;

string header = $@"using System;
using System.Threading.Tasks;
using Xunit;

namespace {TestNamespace}{suiteNamespacePart}
{{
public sealed partial class {suiteClassName}Tests : LinkerTestBase
{{
";
if (newTestSuite)
header += $@"
protected override string TestSuiteName => ""{suiteName}"";
";
return header;
}

public static string GenerateFact (string testCase) {
return $@"
[Fact]
public Task {testCase} ()
{{
return RunTest (allowMissingWarnings: true);
}}
";
}

public static string GenerateClassFooter () {
return $@"
}}
}}";
}
}

[Generator]
public class TestCaseGenerator : ISourceGenerator
{
public const string TestCaseAssembly = "Mono.Linker.Tests.Cases";

public void Execute (GeneratorExecutionContext context)
{
IAssemblySymbol? assemblySymbol = null;

// Find testcase assembly
foreach (var reference in context.Compilation.References) {
ISymbol? assemblyOrModule = context.Compilation.GetAssemblyOrModuleSymbol (reference);
if (assemblyOrModule is IAssemblySymbol asmSym && asmSym.Name == TestCaseAssembly) {
assemblySymbol = asmSym;
break;
}
}
if (assemblySymbol is null || assemblySymbol.GetMetadata () is not AssemblyMetadata assemblyMetadata)
return;

ModuleMetadata moduleMetadata = assemblyMetadata.GetModules ().Single ();
MetadataReader metadataReader = moduleMetadata.GetMetadataReader ();
TestCases testCases = new ();
string suiteName;

// Find test classes
foreach (var typeDefHandle in metadataReader.TypeDefinitions) {
TypeDefinition typeDef = metadataReader.GetTypeDefinition (typeDefHandle);
// Must not be a nested type
if (typeDef.IsNested)
continue;

string ns = metadataReader.GetString (typeDef.Namespace);
// Must be in the testcases namespace
if (!ns.StartsWith (TestCaseAssembly))
continue;

// Must have a Main method
bool hasMain = false;
foreach (var methodDefHandle in typeDef.GetMethods ()) {
MethodDefinition methodDef = metadataReader.GetMethodDefinition (methodDefHandle);
if (metadataReader.GetString (methodDef.Name) == "Main") {
hasMain = true;
break;
}
}
if (!hasMain)
continue;

string testName = metadataReader.GetString (typeDef.Name);
suiteName = ns.Substring (TestCaseAssembly.Length + 1);

testCases.Add (suiteName, testName);
}

TestCases existingTestCases = ((ExistingTestCaseDiscoverer) context.SyntaxContextReceiver!).ExistingTestCases;

// Generate test facts
foreach (var kvp in testCases.Suites) {
suiteName = kvp.Key;
var cases = kvp.Value;

bool newTestSuite = !existingTestCases.Suites.TryGetValue (suiteName, out HashSet<string> existingCases);
var newCases = newTestSuite ? cases : cases.Except (existingCases);
// Skip generating a test class if all testcases in the suite already exist.
if (!newCases.Any ())
continue;

StringBuilder sourceBuilder = new ();
sourceBuilder.Append (TestClassGenerator.GenerateClassHeader (suiteName, newTestSuite));
// Generate facts for all testcases that don't already exist
foreach (var testCase in newCases)
sourceBuilder.Append (TestClassGenerator.GenerateFact (testCase));
sourceBuilder.Append (TestClassGenerator.GenerateClassFooter ());

context.AddSource ($"{suiteName}Tests.g.cs", sourceBuilder.ToString ());
}
}

public void Initialize (GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications (() => new ExistingTestCaseDiscoverer ());
}
}

class ExistingTestCaseDiscoverer : ISyntaxContextReceiver
{
public readonly TestCases ExistingTestCases = new ();

public void OnVisitSyntaxNode (GeneratorSyntaxContext context)
{
if (context.Node is not ClassDeclarationSyntax classSyntax)
return;

if (context.SemanticModel.GetDeclaredSymbol (classSyntax) is not INamedTypeSymbol typeSymbol)
return;

var displayFormat = new SymbolDisplayFormat(
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces);
string typeFullName = typeSymbol.ToDisplayString (displayFormat);

// Ignore types not in the testcase namespace or that don't end with "Tests"
if (!typeFullName.StartsWith (TestClassGenerator.TestNamespace))
return;
if (!typeFullName.EndsWith ("Tests"))
return;

string suiteName = typeFullName.Substring (TestClassGenerator.TestNamespace.Length + 1);
suiteName = suiteName.Substring (0, suiteName.Length - 5);
foreach (var memberSymbol in typeSymbol.GetMembers ()) {
if (memberSymbol is not IMethodSymbol methodSymbol)
continue;
ExistingTestCases.Add (suiteName, methodSymbol.Name);
}
}
}
}
16 changes: 16 additions & 0 deletions test/ILLink.RoslynAnalyzer.Tests/AdvancedTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Threading.Tasks;
using Xunit;

namespace ILLink.RoslynAnalyzer.Tests
{
public sealed partial class AdvancedTests : LinkerTestBase
{
protected override string TestSuiteName => "Advanced";

[Fact (Skip = "https://github.com/dotnet/linker/issues/2415")]
public Task TypeCheckRemoval ()
{
return RunTest (allowMissingWarnings: true);
}
}
}
2 changes: 1 addition & 1 deletion test/ILLink.RoslynAnalyzer.Tests/DataFlowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace ILLink.RoslynAnalyzer.Tests
{
public sealed class DataFlowTests : LinkerTestBase
public sealed partial class DataFlowTests : LinkerTestBase
{
protected override string TestSuiteName => "DataFlow";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Nullable>enable</Nullable>
<DeterministicSourcePaths>false</DeterministicSourcePaths>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

<ItemGroup>
<Compile Remove="$(CompilerGeneratedFilesOutputPath)" />
</ItemGroup>
Comment on lines +7 to +12
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 really understand this - we ask the compiler to write the generated code to disk (I get that, at least for validation purposes and such). But then remove it from the compilation... this is probably because the source generator already added it?

Also - why do we commit the generated files into the source control, if they are always regenerated?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, if we didn't remove them from the compilation they would be defined twice. I'm committing them to source control to make it easier to review changes, and because it sounds like the test UI may have trouble displaying generated tests otherwise. I don't have a strong opinion on it but this is per @agocke's suggestion - happy to remove them if we would rather not check in a bunch of generated sources.

Copy link
Member

Choose a reason for hiding this comment

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

For code that you're expected to interact w/ directly (e.g. tests), I'd prefer to have them checked in. VS Code can't run them individually otherwise

Copy link
Member

@agocke agocke Feb 3, 2022

Choose a reason for hiding this comment

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

Also, to properly enable these test cases, it will be easy to move the file into a non-generated directory. The generator will see that the test file is already present and will stop generating it, and git will understand and track the move operation.


<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="$(MicrosoftCodeAnalysisCSharpAnalyzerTestingXunitVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" Version="$(MicrosoftCodeAnalysisCSharpAnalyzerTestingXunitVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisVersion)" />
<ProjectReference Include="..\..\src\ILLink.CodeFix\ILLink.CodeFixProvider.csproj" />
<ProjectReference Include="..\..\src\ILLink.RoslynAnalyzer\ILLink.RoslynAnalyzer.csproj" />
<ProjectReference Include="../Mono.Linker.Tests.Cases\Mono.Linker.Tests.Cases.csproj" />
<ProjectReference Include="../ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion test/ILLink.RoslynAnalyzer.Tests/LinkAttributesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace ILLink.RoslynAnalyzer.Tests
{
public sealed class LinkAttributesTests : LinkerTestBase
public sealed partial class LinkAttributesTests : LinkerTestBase
{
protected override string TestSuiteName => "LinkAttributes";

Expand Down
4 changes: 2 additions & 2 deletions test/ILLink.RoslynAnalyzer.Tests/LinkerTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ private static readonly (string, string)[] MSBuildProperties = UseMSBuildPropert
MSBuildPropertyOptionNames.EnableSingleFileAnalyzer,
MSBuildPropertyOptionNames.EnableAOTAnalyzer);

protected Task RunTest ([CallerMemberName] string testName = "")
protected Task RunTest ([CallerMemberName] string testName = "", bool allowMissingWarnings = false)
{
return RunTestFile (TestSuiteName, testName, MSBuildProperties);
return RunTestFile (TestSuiteName, testName, allowMissingWarnings, MSBuildProperties);
}
}
}
Loading