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:
Andy Gocke 2016-01-18 15:14:19 -08:00
parent ea43482551
commit c716ad6571
13 changed files with 307 additions and 26 deletions

View 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();
}
}
}

View 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.
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;
}
}
}

View file

@ -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();
}

View file

@ -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)

View file

@ -35,6 +35,8 @@ namespace Microsoft.DotNet.ProjectModel
return Path.GetDirectoryName(ProjectFilePath);
}
}
public AnalyzerOptions AnalyzerOptions { get; set; }
public string Name { get; set; }

View file

@ -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"),

View file

@ -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('"')}\""));

View file

@ -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
{

View file

@ -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}\""));

View file

@ -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)
{

View 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>

View 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
{}
}

View 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": { }
}
}