Merge pull request #927 from agocke/analyzer-support

Add analyzer support
This commit is contained in:
Piotr Puszkiewicz 2016-01-26 19:19:42 -08:00
commit 55e838427f
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": { }
}
}