Add analyzer support
With this change, any referenced analyzer project will be parsed by the project system and the assemblies will be passed down to the compiler. By default, the analyzer language is considered to be "cs". If another language is used, the "languageID" option should be specified inside the "analyzerOptions" section of the project.json file. Resolves #83
This commit is contained in:
parent
ea43482551
commit
c716ad6571
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