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:
Родитель
1e6660185e
Коммит
3c7c7b63de
|
@ -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
|
||||
|
|
|
@ -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>
|
Загрузка…
Ссылка в новой задаче