Fail build early and warn in IDE when resource not found

Instead of waiting for the full build to fail, warn users of missing resources much earlier, right in the editor, but also fail the build with the proper error, well before aapt runs.

In order for the build to also account for this new analyzer, we move the analyzer assembly to the MSBuild targets location, now under MSBuild\Xamarin\CodeAnalysis instead of the root Xamarin dir. Some tweaks were required on the VSIX project to properly collect the files and their target location.
This commit is contained in:
Daniel Cazzulino 2019-03-31 11:43:58 -03:00
Родитель 1e6660185e
Коммит 3c7c7b63de
14 изменённых файлов: 252 добавлений и 38 удалений

Просмотреть файл

@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
azure-pipelines.yml = azure-pipelines.yml
src\Directory.Build.props = src\Directory.Build.props
src\Directory.Build.targets = src\Directory.Build.targets
src\GenerateInternalsVisibleTo.targets = src\GenerateInternalsVisibleTo.targets
src\GitInfo.txt = src\GitInfo.txt
src\global.json = src\global.json
nuget.config = nuget.config
@ -23,6 +24,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.CodeAnalysis.Remote
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.CodeAnalysis.Windows", "src\Xamarin.CodeAnalysis.Windows\Xamarin.CodeAnalysis.Windows.csproj", "{5409EFB9-CF35-47C4-AC27-74A1122132AF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{54758F73-0AF2-4AF2-A873-82F7A5EB5450}"
ProjectSection(SolutionItems) = preProject
docs\XAA1001.md = docs\XAA1001.md
docs\XAA1002.md = docs\XAA1002.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU

25
docs/XAA1002.md Normal file
Просмотреть файл

@ -0,0 +1,25 @@
# XAA1002
## Cause
A string references a resource identifier that was not found in the current compilation.
## Rule description
No resource found that matches the given name.
## How to fix violations
To fix a violation of this rule, use the provided completion list to discover the existing resource
identifiers, or add the missing identifier to the respective `.xml` resource file.
## How to suppress violations
```csharp
[SuppressMessage("Xamarin.CodeAnalysis", "XAA1002:ResourceIdentifierNotFound", Justification = "Reviewed.")]
```
```csharp
#pragma warning disable XAA1002 // ResourceIdentifierNotFound
#pragma warning restore XAA1002 // ResourceIdentifierNotFound
```

Просмотреть файл

@ -20,6 +20,9 @@
<GitSkipCache Condition="$(CI)">true</GitSkipCache>
<Configuration Condition="'$(Configuration)' == '' and $(CI)">Release</Configuration>
<Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
<!-- We don't want to run code analysis for ourselves. -->
<ImportXamarinCodeAnalysisTargets>false</ImportXamarinCodeAnalysisTargets>
<DefineConstants Condition="$(CI)">CI;$(DefineConstants)</DefineConstants>
</PropertyGroup>
<!-- Redeclared by GitInfo -->

Просмотреть файл

@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
@ -25,8 +22,15 @@ namespace Xamarin.CodeAnalysis.Tests
public class MainActivity : Activity
{
}
", XAA1001StringLiteralToResource.DiagnosticId)]
public async Task can_get_diagnostics(string code, string diagnosticId)
", typeof(XAA1001StringLiteralToResource))]
[InlineData(@"using Android.App;
[Activity(Label = ""@string/foo"")]
public class MainActivity : Activity
{
}
", typeof(XAA1002ResourceIdentifierNotFound))]
public async Task can_get_diagnostics(string code, Type analyzerType)
{
var workspace = new AdhocWorkspace();
var document = workspace
@ -35,36 +39,54 @@ public class MainActivity : Activity
.WithMetadataReferences(Directory
.EnumerateFiles("MonoAndroid", "*.dll")
.Select(dll => MetadataReference.CreateFromFile(dll)))
.AddAdditionalDocument("strings.xml", @"<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name='app_name'>TestApp</string>
</resources>", new[] { "Resources", "values" }, "Resources\\values\\strings.xml")
.Project
.AddAdditionalDocument("styles.xml", @"<?xml version='1.0' encoding='utf-8'?>
<resources>
<style name='AppTheme' parent='Theme.AppCompat.Light.DarkActionBar' />
</resources>", new[] { "Resources", "values" }, "Resources\\values\\styles.xml")
.AddDocument("Resource.designer.cs", @"[assembly: global::Android.Runtime.ResourceDesignerAttribute(""MyApp.Resource"", IsApplication=true)]
namespace MyApp
{
[System.CodeDom.Compiler.GeneratedCodeAttribute(""Xamarin.Android.Build.Tasks"", ""1.0.0.0"")]
public partial class Resource
{
public partial class String
{
public const int app_name = 2130968578;
public const int app_title = 2130968579;
}
public partial class Style
{
public const int AppTheme = 2131034114;
}
public partial class Drawable
{
public const int design_fab_background = 2131296343;
}
public partial class Mipmap
{
public const int ic_launcher = 2130837506;
}
}
}")
.Project
.AddDocument("TestDocument.cs", code.Replace("`", ""));
var compilation = await document.Project.GetCompilationAsync(TimeoutToken(5));
var withAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create<DiagnosticAnalyzer>(new XAA1001StringLiteralToResource()));
var analyzer = (DiagnosticAnalyzer)Activator.CreateInstance(analyzerType);
var withAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer));
var diagnostic = (await withAnalyzers.GetAnalyzerDiagnosticsAsync())
.Where(d => d.Id == diagnosticId)
.Where(d => analyzer.SupportedDiagnostics.Any(x => x.Id == d.Id))
.OrderBy(d => d.Location.SourceSpan.Start)
.FirstOrDefault() ?? throw new ArgumentException($"Analyzer did not produce diagnostic {diagnosticId}.");
.FirstOrDefault() ?? throw new ArgumentException($"Analyzer did not produce diagnostic(s) {string.Join(", ", analyzer.SupportedDiagnostics.Select(d => d.Id))}.");
var actions = new List<CodeAction>();
var context = new CodeFixContext(document, diagnostic, (a, d) => actions.Add(a), TimeoutToken(5));
await new XAA1001CodeFixProvider().RegisterCodeFixesAsync(context);
// TODO: test code fix?
//var actions = new List<CodeAction>();
//var context = new CodeFixContext(document, diagnostic, (a, d) => actions.Add(a), TimeoutToken(5));
//await new XAA1001CodeFixProvider().RegisterCodeFixesAsync(context);
var changed = actions
.SelectMany(x => x.GetOperationsAsync(TimeoutToken(2)).Result)
.OfType<ApplyChangesOperation>()
.First()
.ChangedSolution;
//var changed = actions
// .SelectMany(x => x.GetOperationsAsync(TimeoutToken(2)).Result)
// .OfType<ApplyChangesOperation>()
// .First()
// .ChangedSolution;
var changes = changed.GetChanges(document.Project.Solution);
//var changes = changed.GetChanges(document.Project.Solution);
}
}

Просмотреть файл

@ -1,9 +0,0 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<XamarinCodeAnalysisTargets Condition="'$(XamarinCodeAnalysisTargets)' == ''">$(MSBuildExtensionsPath)\Xamarin\Xamarin.CodeAnalysis.targets</XamarinCodeAnalysisTargets>
</PropertyGroup>
<Import Condition="Exists('$(XamarinCodeAnalysisTargets)')" Project="$(XamarinCodeAnalysisTargets)" />
</Project>

Просмотреть файл

@ -48,6 +48,18 @@
<ItemGroup>
<ProjectReference Include="..\Xamarin.CodeAnalysis\Xamarin.CodeAnalysis.csproj">
<Name>Xamarin.CodeAnalysis</Name>
<!-- For inclusion in the VSIX install path, we don't want the .targets -->
<AdditionalProperties>ExcludeTargets=true</AdditionalProperties>
</ProjectReference>
<ProjectReference Include="..\Xamarin.CodeAnalysis\Xamarin.CodeAnalysis.csproj">
<AdditionalProperties>BuildReference=false</AdditionalProperties>
<Name>Xamarin.CodeAnalysis</Name>
<BuildProject>false</BuildProject>
<!-- For inclusion in the MSBuild folder, pass extra metadata -->
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<VSIXSubPath>Xamarin\CodeAnalysis</VSIXSubPath>
<InstallRoot>MSBuild</InstallRoot>
<SymLink>true</SymLink>
</ProjectReference>
</ItemGroup>
@ -62,7 +74,6 @@
<ItemGroup>
<Content Include="*.targets" IncludeInVSIX="true" VSIXSubPath="Xamarin" SymLink="true" />
<Content Update="Xamarin.CodeAnalysis.ImportAfter.targets" VSIXSubPath="Current\Microsoft.Common.Targets\ImportAfter" />
<Content Update="@(Content)" Condition="$(CI)" InstallRoot="MSBuild" />
<None Remove="Resources\*.*" />
<Content Include="Resources\*.*" IncludeInVSIX="true" CopyToOutputDirectory="PreserveNewest" />
@ -94,12 +105,25 @@
<Target Name="IsSystemComponent" Returns="$(IsSystemComponent)" />
<Target Name="IsCI" Returns="$(CI)" />
<!--
<PropertyGroup Condition="'$(OS)' == 'Windows_NT'">
<BuildDependsOn Condition="!$(CI)">
$(BuildDependsOn);
SymLink
</BuildDependsOn>
</PropertyGroup>
-->
<Target Name="ApplyVSIXSubPathOverride" AfterTargets="GetVsixSourceItems">
<ItemGroup>
<VSIXSourceItem Update="@(VSIXSourceItem)" Condition="'%(VSIXSourceItem.VSIXSubPathOverride)' != ''">
<VSIXSubPath>%(VSIXSourceItem.VSIXSubPathOverride)</VSIXSubPath>
</VSIXSourceItem>
<VSIXSourceItem Update="@(VSIXSourceItem)" Condition="'%(VSIXSourceItem.VSIXSubPath)' != ''">
<TargetPath />
</VSIXSourceItem>
</ItemGroup>
</Target>
<Target Name="SymLink" DependsOnTargets="IsAdministrator;CollectLinkItems;ReplaceLinkItems" />

Просмотреть файл

@ -1,4 +1,5 @@
using System.Runtime.InteropServices;
using System;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.Shell;
[assembly: ComVisible(false)]

Просмотреть файл

@ -86,5 +86,32 @@ namespace Xamarin.CodeAnalysis.Properties {
return ResourceManager.GetString("XAA1001_Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No resource found that matches the given name..
/// </summary>
internal static string XAA1002_Description {
get {
return ResourceManager.GetString("XAA1002_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No resource found that matches the given name..
/// </summary>
internal static string XAA1002_MessageFormat {
get {
return ResourceManager.GetString("XAA1002_MessageFormat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Resource id must exist in a resource file.
/// </summary>
internal static string XAA1002_Title {
get {
return ResourceManager.GetString("XAA1002_Title", resourceCulture);
}
}
}
}

Просмотреть файл

@ -126,4 +126,13 @@
<data name="XAA1001_Title" xml:space="preserve">
<value>Move string to resource</value>
</data>
<data name="XAA1002_Description" xml:space="preserve">
<value>No resource found that matches the given name.</value>
</data>
<data name="XAA1002_MessageFormat" xml:space="preserve">
<value>No resource found that matches the given name.</value>
</data>
<data name="XAA1002_Title" xml:space="preserve">
<value>Resource id must exist in a resource file</value>
</data>
</root>

Просмотреть файл

@ -0,0 +1,85 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Xamarin.CodeAnalysis.Properties;
using static Xamarin.CodeAnalysis.LocalizableString;
namespace Xamarin.CodeAnalysis
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class XAA1002ResourceIdentifierNotFound : DiagnosticAnalyzer
{
/// <summary>
/// The ID for diagnostics produced by the <see cref="XAA1001StringLiteralToResource"/> analyzer.
/// </summary>
public const string DiagnosticId = "XAA1002";
const string HelpLink = "https://github.com/xamarin/CodeAnalysis/blob/master/docs/XAA1002.md";
static readonly DiagnosticDescriptor Descriptor =
new DiagnosticDescriptor(DiagnosticId,
Localizable(nameof(Resources.XAA1002_Title)),
Localizable(nameof(Resources.XAA1002_MessageFormat)),
Constants.AnalyzerCategory,
Microsoft.CodeAnalysis.DiagnosticSeverity.Error,
true,
Localizable(nameof(Resources.XAA1002_Description)),
HelpLink);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Descriptor);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterSyntaxNodeAction(AnalyzeLiteral, Microsoft.CodeAnalysis.CSharp.SyntaxKind.StringLiteralExpression);
}
void AnalyzeLiteral(SyntaxNodeAnalysisContext context)
{
if (context.Node is LiteralExpressionSyntax literal &&
context.Node.Parent is AttributeArgumentSyntax argument &&
// TODO: we support only property assignment in attributes we know
// about, we want to be conservative in the errors we report for now.
argument.NameEquals != null &&
context.Node.Parent?.Parent?.Parent is AttributeSyntax attribute &&
literal.GetText().ToString().Trim('"') is string value &&
value.StartsWith("@") &&
value.IndexOf('/') is int slash &&
slash != -1)
{
var category = value.Substring(1, slash - 1);
var identifier = value.Substring(slash + 1);
var compilation = context.Compilation;
var resourceDesignerAttribute = compilation.Assembly.GetAttributes().FirstOrDefault(attr =>
attr.AttributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Android.Runtime.ResourceDesignerAttribute");
if (resourceDesignerAttribute != null && resourceDesignerAttribute.ConstructorArguments.Any())
{
var resourceDesigner = compilation.GetTypeByMetadataName((string)resourceDesignerAttribute.ConstructorArguments.First().Value);
if (resourceDesigner != null)
{
var resourceSymbol = resourceDesigner.GetTypeMembers().FirstOrDefault(x => x.Name.Equals(category, StringComparison.OrdinalIgnoreCase));
if (resourceSymbol != null)
{
var member = resourceSymbol.GetMembers(identifier).FirstOrDefault();
if (member == null)
{
// TODO: report?
context.ReportDiagnostic(Diagnostic.Create(Descriptor, context.Node.GetLocation()));
}
}
else
{
// TODO: report?
context.ReportDiagnostic(Diagnostic.Create(Descriptor, context.Node.GetLocation()));
}
}
}
}
}
}
}

Просмотреть файл

@ -4,4 +4,4 @@
<AdditionalFiles Include="@(AndroidResource -> WithMetadataValue('RelativeDir', 'Resources\values\'))" />
</ItemGroup>
</Project>
</Project>

Просмотреть файл

@ -0,0 +1,8 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ImportXamarinCodeAnalysisTargets Condition="'$(ImportXamarinCodeAnalysisTargets)' == ''">true</ImportXamarinCodeAnalysisTargets>
<XamarinCodeAnalysisTargets Condition="'$(XamarinCodeAnalysisTargets)' == ''">$(MSBuildExtensionsPath)\Xamarin\CodeAnalysis\Xamarin.CodeAnalysis.targets</XamarinCodeAnalysisTargets>
</PropertyGroup>
<Import Condition="Exists('$(XamarinCodeAnalysisTargets)') and '$(ImportXamarinCodeAnalysisTargets)' == 'true'" Project="$(XamarinCodeAnalysisTargets)" />
</Project>

Просмотреть файл

@ -3,6 +3,8 @@
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<!-- Allow our additional item metadata to propagate to the calling project -->
<MSBuildDisableGetCopyToOutputDirectoryItemsOptimization>true</MSBuildDisableGetCopyToOutputDirectoryItemsOptimization>
</PropertyGroup>
<ItemGroup>
@ -26,4 +28,10 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Update="*.targets" Condition="'$(ExcludeTargets)' != 'true'" CopyToOutputDirectory="PreserveNewest" />
<None Update="Xamarin.CodeAnalysis.ImportAfter.targets"
VSIXSubPathOverride="Current\Microsoft.Common.Targets\ImportAfter" />
</ItemGroup>
</Project>

Просмотреть файл

@ -2,4 +2,8 @@
<Import Project="Xamarin.CodeAnalysis.Android.targets" Condition="'$(TargetFrameworkIdentifier)' == 'MonoAndroid'" />
<ItemGroup>
<Analyzer Include="$(MSBuildThisFileDirectory)Xamarin.CodeAnalysis.dll" />
</ItemGroup>
</Project>