Merge pull request #927 from agocke/analyzer-support
Add analyzer support
This commit is contained in:
commit
55e838427f
13 changed files with 307 additions and 26 deletions
42
src/Microsoft.DotNet.ProjectModel/AnalyzerOptions.cs
Normal file
42
src/Microsoft.DotNet.ProjectModel/AnalyzerOptions.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) .NET Foundation and contributors. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace Microsoft.DotNet.ProjectModel
|
||||
{
|
||||
public class AnalyzerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The identifier indicating the project language as defined by NuGet.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See https://docs.nuget.org/create/analyzers-conventions for valid values
|
||||
/// </remarks>
|
||||
public string LanguageId { get; set; }
|
||||
|
||||
public static bool operator ==(AnalyzerOptions left, AnalyzerOptions right)
|
||||
{
|
||||
return left.LanguageId == right.LanguageId;
|
||||
}
|
||||
|
||||
public static bool operator !=(AnalyzerOptions left, AnalyzerOptions right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var options = obj as AnalyzerOptions;
|
||||
return obj != null && (this == options);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return LanguageId.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) .NET Foundation and contributors. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using NuGet.Frameworks;
|
||||
|
||||
namespace Microsoft.DotNet.ProjectModel.Compilation
|
||||
{
|
||||
public class AnalyzerReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The fully-qualified path to the analyzer assembly.
|
||||
/// </summary>
|
||||
public string AssemblyPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The supported language of the analyzer assembly.
|
||||
/// </summary>
|
||||
public string AnalyzerLanguage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The required framework for hosting the analyzer assembly.
|
||||
/// </summary>
|
||||
public NuGetFramework RequiredFramework { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The required runtime for hosting the analyzer assembly.
|
||||
/// </summary>
|
||||
public string RuntimeIdentifier { get; }
|
||||
|
||||
public AnalyzerReference(
|
||||
string assembly,
|
||||
NuGetFramework framework,
|
||||
string language,
|
||||
string runtimeIdentifier)
|
||||
{
|
||||
AnalyzerLanguage = language;
|
||||
AssemblyPath = assembly;
|
||||
RequiredFramework = framework;
|
||||
RuntimeIdentifier = runtimeIdentifier;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,15 +33,26 @@ namespace Microsoft.DotNet.ProjectModel.Compilation
|
|||
/// Gets a list of fully-qualified paths to source code file references
|
||||
/// </summary>
|
||||
public IEnumerable<string> SourceReferences { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of analyzers provided by this export.
|
||||
/// </summary>
|
||||
public IEnumerable<AnalyzerReference> AnalyzerReferences { get; }
|
||||
|
||||
public LibraryExport(LibraryDescription library, IEnumerable<LibraryAsset> compileAssemblies, IEnumerable<string> sourceReferences, IEnumerable<LibraryAsset> runtimeAssemblies, IEnumerable<LibraryAsset> nativeLibraries)
|
||||
{
|
||||
Library = library;
|
||||
CompilationAssemblies = compileAssemblies;
|
||||
SourceReferences = sourceReferences;
|
||||
RuntimeAssemblies = runtimeAssemblies;
|
||||
NativeLibraries = nativeLibraries;
|
||||
}
|
||||
public LibraryExport(LibraryDescription library,
|
||||
IEnumerable<LibraryAsset> compileAssemblies,
|
||||
IEnumerable<string> sourceReferences,
|
||||
IEnumerable<LibraryAsset> runtimeAssemblies,
|
||||
IEnumerable<LibraryAsset> nativeLibraries,
|
||||
IEnumerable<AnalyzerReference> analyzers)
|
||||
{
|
||||
Library = library;
|
||||
CompilationAssemblies = compileAssemblies;
|
||||
SourceReferences = sourceReferences;
|
||||
RuntimeAssemblies = runtimeAssemblies;
|
||||
NativeLibraries = nativeLibraries;
|
||||
AnalyzerReferences = analyzers;
|
||||
}
|
||||
|
||||
private string DebuggerDisplay => Library.Identity.ToString();
|
||||
}
|
||||
|
|
|
@ -79,7 +79,9 @@ namespace Microsoft.DotNet.ProjectModel.Compilation
|
|||
|
||||
var compilationAssemblies = new List<LibraryAsset>();
|
||||
var sourceReferences = new List<string>();
|
||||
var analyzerReferences = new List<AnalyzerReference>();
|
||||
var libraryExport = GetExport(library);
|
||||
|
||||
|
||||
// We need to filter out source references from non-root libraries,
|
||||
// so we rebuild the library export
|
||||
|
@ -91,16 +93,15 @@ namespace Microsoft.DotNet.ProjectModel.Compilation
|
|||
}
|
||||
}
|
||||
|
||||
// Source and analyzer references are not transitive
|
||||
if (library.Parents.Contains(_rootProject))
|
||||
{
|
||||
// Only process source references for direct dependencies
|
||||
foreach (var sourceReference in libraryExport.SourceReferences)
|
||||
{
|
||||
sourceReferences.Add(sourceReference);
|
||||
}
|
||||
sourceReferences.AddRange(libraryExport.SourceReferences);
|
||||
analyzerReferences.AddRange(libraryExport.AnalyzerReferences);
|
||||
}
|
||||
|
||||
yield return new LibraryExport(library, compilationAssemblies, sourceReferences, libraryExport.RuntimeAssemblies, libraryExport.NativeLibraries);
|
||||
yield return new LibraryExport(library, compilationAssemblies, sourceReferences,
|
||||
libraryExport.RuntimeAssemblies, libraryExport.NativeLibraries, analyzerReferences);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,8 +142,11 @@ namespace Microsoft.DotNet.ProjectModel.Compilation
|
|||
{
|
||||
sourceReferences.Add(sharedSource);
|
||||
}
|
||||
|
||||
var analyzers = GetAnalyzerReferences(package);
|
||||
|
||||
return new LibraryExport(package, compileAssemblies, sourceReferences, runtimeAssemblies, nativeLibraries);
|
||||
return new LibraryExport(package, compileAssemblies,
|
||||
sourceReferences, runtimeAssemblies, nativeLibraries, analyzers);
|
||||
}
|
||||
|
||||
private LibraryExport ExportProject(ProjectDescription project)
|
||||
|
@ -166,8 +170,11 @@ namespace Microsoft.DotNet.ProjectModel.Compilation
|
|||
sourceReferences.Add(sharedFile);
|
||||
}
|
||||
|
||||
// No support for ref or native in projects, so runtimeAssemblies is just the same as compileAssemblies and nativeLibraries are empty
|
||||
return new LibraryExport(project, compileAssemblies, sourceReferences, compileAssemblies, Enumerable.Empty<LibraryAsset>());
|
||||
// No support for ref or native in projects, so runtimeAssemblies is
|
||||
// just the same as compileAssemblies and nativeLibraries are empty
|
||||
// Also no support for analyzer projects
|
||||
return new LibraryExport(project, compileAssemblies, sourceReferences,
|
||||
compileAssemblies, Array.Empty<LibraryAsset>(), Array.Empty<AnalyzerReference>());
|
||||
}
|
||||
|
||||
private static string ResolvePath(Project project, string configuration, string path)
|
||||
|
@ -191,11 +198,12 @@ namespace Microsoft.DotNet.ProjectModel.Compilation
|
|||
return new LibraryExport(
|
||||
library,
|
||||
string.IsNullOrEmpty(library.Path) ?
|
||||
Enumerable.Empty<LibraryAsset>() :
|
||||
Array.Empty<LibraryAsset>() :
|
||||
new[] { new LibraryAsset(library.Identity.Name, library.Path, library.Path) },
|
||||
Enumerable.Empty<string>(),
|
||||
Enumerable.Empty<LibraryAsset>(),
|
||||
Enumerable.Empty<LibraryAsset>());
|
||||
Array.Empty<string>(),
|
||||
Array.Empty<LibraryAsset>(),
|
||||
Array.Empty<LibraryAsset>(),
|
||||
Array.Empty<AnalyzerReference>());
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetSharedSources(PackageDescription package)
|
||||
|
@ -206,6 +214,74 @@ namespace Microsoft.DotNet.ProjectModel.Compilation
|
|||
.Where(path => path.StartsWith("shared" + Path.DirectorySeparatorChar))
|
||||
.Select(path => Path.Combine(package.Path, path));
|
||||
}
|
||||
|
||||
private IEnumerable<AnalyzerReference> GetAnalyzerReferences(PackageDescription package)
|
||||
{
|
||||
var analyzers = package
|
||||
.Library
|
||||
.Files
|
||||
.Where(path => path.StartsWith("analyzers" + Path.DirectorySeparatorChar) &&
|
||||
path.EndsWith(".dll"));
|
||||
|
||||
var analyzerRefs = new List<AnalyzerReference>();
|
||||
// See https://docs.nuget.org/create/analyzers-conventions for the analyzer
|
||||
// NuGet specification
|
||||
foreach (var analyzer in analyzers)
|
||||
{
|
||||
var specifiers = analyzer.Split(Path.DirectorySeparatorChar);
|
||||
|
||||
var assemblyPath = Path.Combine(package.Path, analyzer);
|
||||
|
||||
// $/analyzers/{Framework Name}{Version}/{Supported Architecture}/{Supported Programming Language}/{Analyzer}.dll
|
||||
switch (specifiers.Length)
|
||||
{
|
||||
// $/analyzers/{analyzer}.dll
|
||||
case 2:
|
||||
analyzerRefs.Add(new AnalyzerReference(
|
||||
assembly: assemblyPath,
|
||||
framework: null,
|
||||
language: null,
|
||||
runtimeIdentifier: null
|
||||
));
|
||||
break;
|
||||
|
||||
// $/analyzers/{framework}/{analyzer}.dll
|
||||
case 3:
|
||||
analyzerRefs.Add(new AnalyzerReference(
|
||||
assembly: assemblyPath,
|
||||
framework: NuGetFramework.Parse(specifiers[1]),
|
||||
language: null,
|
||||
runtimeIdentifier: null
|
||||
));
|
||||
break;
|
||||
|
||||
// $/analyzers/{framework}/{language}/{analyzer}.dll
|
||||
case 4:
|
||||
analyzerRefs.Add(new AnalyzerReference(
|
||||
assembly: assemblyPath,
|
||||
framework: NuGetFramework.Parse(specifiers[1]),
|
||||
language: specifiers[2],
|
||||
runtimeIdentifier: null
|
||||
));
|
||||
break;
|
||||
|
||||
// $/analyzers/{framework}/{runtime}/{language}/{analyzer}.dll
|
||||
case 5:
|
||||
analyzerRefs.Add(new AnalyzerReference(
|
||||
assembly: assemblyPath,
|
||||
framework: NuGetFramework.Parse(specifiers[1]),
|
||||
language: specifiers[3],
|
||||
runtimeIdentifier: specifiers[2]
|
||||
));
|
||||
break;
|
||||
|
||||
// Anything less than 2 specifiers or more than 4 is
|
||||
// illegal according to the specification and will be
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
return analyzerRefs;
|
||||
}
|
||||
|
||||
|
||||
private void PopulateAssets(PackageDescription package, IEnumerable<LockFileItem> section, IList<LibraryAsset> assets)
|
||||
|
|
|
@ -35,6 +35,8 @@ namespace Microsoft.DotNet.ProjectModel
|
|||
return Path.GetDirectoryName(ProjectFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
public AnalyzerOptions AnalyzerOptions { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
|
|
|
@ -355,7 +355,9 @@ namespace Microsoft.DotNet.ProjectModel
|
|||
private void BuildTargetFrameworksAndConfigurations(Project project, JsonObject projectJsonObject, ICollection<DiagnosticMessage> diagnostics)
|
||||
{
|
||||
// Get the shared compilationOptions
|
||||
project._defaultCompilerOptions = GetCompilationOptions(projectJsonObject) ?? new CommonCompilerOptions();
|
||||
project._defaultCompilerOptions = GetCompilationOptions(projectJsonObject,
|
||||
project)
|
||||
?? new CommonCompilerOptions();
|
||||
|
||||
project._defaultTargetFrameworkConfiguration = new TargetFrameworkInformation
|
||||
{
|
||||
|
@ -392,7 +394,8 @@ namespace Microsoft.DotNet.ProjectModel
|
|||
{
|
||||
foreach (var configKey in configurationsSection.Keys)
|
||||
{
|
||||
var compilerOptions = GetCompilationOptions(configurationsSection.ValueAsJsonObject(configKey));
|
||||
var compilerOptions = GetCompilationOptions(configurationsSection.ValueAsJsonObject(configKey),
|
||||
project);
|
||||
|
||||
// Only use this as a configuration if it's not a target framework
|
||||
project._compilerOptionsByConfiguration[configKey] = compilerOptions;
|
||||
|
@ -449,7 +452,7 @@ namespace Microsoft.DotNet.ProjectModel
|
|||
private bool BuildTargetFrameworkNode(Project project, string frameworkKey, JsonObject frameworkValue)
|
||||
{
|
||||
// If no compilation options are provided then figure them out from the node
|
||||
var compilerOptions = GetCompilationOptions(frameworkValue) ??
|
||||
var compilerOptions = GetCompilationOptions(frameworkValue, project) ??
|
||||
new CommonCompilerOptions();
|
||||
|
||||
var frameworkName = NuGetFramework.Parse(frameworkKey);
|
||||
|
@ -515,7 +518,7 @@ namespace Microsoft.DotNet.ProjectModel
|
|||
return true;
|
||||
}
|
||||
|
||||
private static CommonCompilerOptions GetCompilationOptions(JsonObject rawObject)
|
||||
private static CommonCompilerOptions GetCompilationOptions(JsonObject rawObject, Project project)
|
||||
{
|
||||
var rawOptions = rawObject.ValueAsJsonObject("compilationOptions");
|
||||
if (rawOptions == null)
|
||||
|
@ -523,6 +526,37 @@ namespace Microsoft.DotNet.ProjectModel
|
|||
return null;
|
||||
}
|
||||
|
||||
var analyzerOptionsJson = rawOptions.Value("analyzerOptions") as JsonObject;
|
||||
if (analyzerOptionsJson != null)
|
||||
{
|
||||
var analyzerOptions = new AnalyzerOptions();
|
||||
|
||||
foreach (var key in analyzerOptionsJson.Keys)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "languageId":
|
||||
var languageId = analyzerOptionsJson.ValueAsString(key);
|
||||
if (languageId == null)
|
||||
{
|
||||
throw FileFormatException.Create(
|
||||
"The analyzer languageId must be a string",
|
||||
analyzerOptionsJson.Value(key),
|
||||
project.ProjectFilePath);
|
||||
}
|
||||
analyzerOptions.LanguageId = languageId;
|
||||
break;
|
||||
|
||||
default:;
|
||||
throw FileFormatException.Create(
|
||||
$"Unrecognized analyzerOption key: {key}",
|
||||
project.ProjectFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
project.AnalyzerOptions = analyzerOptions;
|
||||
}
|
||||
|
||||
return new CommonCompilerOptions
|
||||
{
|
||||
Defines = rawOptions.ValueAsStringArray("define"),
|
||||
|
|
|
@ -29,6 +29,7 @@ namespace Microsoft.DotNet.Tools.Compiler.Csc
|
|||
IReadOnlyList<string> references = Array.Empty<string>();
|
||||
IReadOnlyList<string> resources = Array.Empty<string>();
|
||||
IReadOnlyList<string> sources = Array.Empty<string>();
|
||||
IReadOnlyList<string> analyzers = Array.Empty<string>();
|
||||
string outputName = null;
|
||||
var help = false;
|
||||
var returnCode = 0;
|
||||
|
@ -50,6 +51,8 @@ namespace Microsoft.DotNet.Tools.Compiler.Csc
|
|||
syntax.DefineOption("out", ref outputName, "Name of the output assembly");
|
||||
|
||||
syntax.DefineOptionList("reference", ref references, "Path to a compiler metadata reference");
|
||||
|
||||
syntax.DefineOptionList("analyzer", ref analyzers, "Path to an analyzer assembly");
|
||||
|
||||
syntax.DefineOptionList("resource", ref resources, "Resources to embed");
|
||||
|
||||
|
@ -96,6 +99,7 @@ namespace Microsoft.DotNet.Tools.Compiler.Csc
|
|||
allArgs.Add($"-out:\"{outputName.Trim('"')}\"");
|
||||
}
|
||||
|
||||
allArgs.AddRange(analyzers.Select(a => $"-a:\"{a.Trim('"')}\""));
|
||||
allArgs.AddRange(references.Select(r => $"-r:\"{r.Trim('"')}\""));
|
||||
allArgs.AddRange(resources.Select(resource => $"-resource:{resource.Trim('"')}"));
|
||||
allArgs.AddRange(sources.Select(s => $"\"{s.Trim('"')}\""));
|
||||
|
|
|
@ -27,6 +27,14 @@ namespace Microsoft.DotNet.Tools.Compiler
|
|||
|
||||
return compilerName;
|
||||
}
|
||||
|
||||
public static string ResolveLanguageId(ProjectContext context)
|
||||
{
|
||||
var languageId = context.ProjectFile.AnalyzerOptions?.LanguageId;
|
||||
languageId = languageId ?? "cs";
|
||||
|
||||
return languageId;
|
||||
}
|
||||
|
||||
public struct NonCultureResgenIO
|
||||
{
|
||||
|
|
|
@ -6,7 +6,6 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Dnx.Runtime.Common.CommandLine;
|
||||
using Microsoft.DotNet.Cli.Compiler.Common;
|
||||
using Microsoft.DotNet.Cli.Utils;
|
||||
using Microsoft.DotNet.ProjectModel;
|
||||
|
@ -212,6 +211,7 @@ namespace Microsoft.DotNet.Tools.Compiler
|
|||
};
|
||||
|
||||
var compilationOptions = CompilerUtil.ResolveCompilationOptions(context, args.ConfigValue);
|
||||
var languageId = CompilerUtil.ResolveLanguageId(context);
|
||||
|
||||
var references = new List<string>();
|
||||
|
||||
|
@ -239,6 +239,11 @@ namespace Microsoft.DotNet.Tools.Compiler
|
|||
}
|
||||
|
||||
compilerArgs.AddRange(dependency.SourceReferences.Select(s => $"\"{s}\""));
|
||||
|
||||
// Add analyzer references
|
||||
compilerArgs.AddRange(dependency.AnalyzerReferences
|
||||
.Where(a => a.AnalyzerLanguage == languageId)
|
||||
.Select(a => $"--analyzer:\"{a.AssemblyPath}\""));
|
||||
}
|
||||
|
||||
compilerArgs.AddRange(references.Select(r => $"--reference:\"{r}\""));
|
||||
|
|
|
@ -38,6 +38,24 @@ namespace Microsoft.DotNet.Tools.Publish.Tests
|
|||
Assert.True(File.Exists(outputXml));
|
||||
Assert.Contains("Gets the message from the helper", File.ReadAllText(outputXml));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LibraryWithAnalyzer()
|
||||
{
|
||||
var root = Temp.CreateDirectory();
|
||||
var testLibDir = root.CreateDirectory("TestLibraryWithAnalyzer");
|
||||
|
||||
CopyProjectToTempDir(Path.Combine(_testProjectsRoot, "TestLibraryWithAnalyzer"), testLibDir);
|
||||
RunRestore(testLibDir.Path);
|
||||
|
||||
// run compile
|
||||
var outputDir = Path.Combine(testLibDir.Path, "bin");
|
||||
var testProject = GetProjectPath(testLibDir);
|
||||
var buildCmd = new BuildCommand(testProject, output: outputDir);
|
||||
var result = buildCmd.ExecuteWithCapturedOutput();
|
||||
result.Should().Pass();
|
||||
Assert.Contains("CA1018", result.StdErr);
|
||||
}
|
||||
|
||||
private void CopyProjectToTempDir(string projectDir, TempDirectory tempDir)
|
||||
{
|
||||
|
|
9
test/TestProjects/TestLibraryWithAnalyzer/NuGet.Config
Executable file
9
test/TestProjects/TestLibraryWithAnalyzer/NuGet.Config
Executable file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
|
||||
<clear />
|
||||
<add key="dotnet-core" value="https://www.myget.org/F/dotnet-core/api/v3/index.json" />
|
||||
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
15
test/TestProjects/TestLibraryWithAnalyzer/Program.cs
Executable file
15
test/TestProjects/TestLibraryWithAnalyzer/Program.cs
Executable file
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
|
||||
namespace ConsoleApplication
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Hello World!");
|
||||
}
|
||||
}
|
||||
|
||||
public class TT : Attribute
|
||||
{}
|
||||
}
|
15
test/TestProjects/TestLibraryWithAnalyzer/project.json
Executable file
15
test/TestProjects/TestLibraryWithAnalyzer/project.json
Executable file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"version": "1.0.0-*",
|
||||
"compilationOptions": {
|
||||
"emitEntryPoint": true
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"NETStandard.Library": "1.0.0-rc2-23704",
|
||||
"System.Runtime.Analyzers": { "version": "1.1.0", "type": "build" }
|
||||
},
|
||||
|
||||
"frameworks": {
|
||||
"dnxcore50": { }
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue