-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added analyzer project and a few initial analyzers (#1905)
* Added analyzer project for analysis and code gen * Added comment why netstandard2.0 is used for analyzers Analyzers MUST target netstandard2.0, do not update unless Microsoft (MSDN) says it's possible. * Added JetBrains.Annotation package and applied it to Validate.NotNull API Co-authored-by: Jannify <[email protected]> * Changed where Validate.IsTrue was used for null checking * Added codefix for null propagation on UnityEngine.Object * Added "is null" analyzer for unity object lifetime * Added code fixer for "?." on UnityEngine.Object * Fixed JetBrains namespace collision with UnityEngine.Core * Fixed invalid null propagation on UnityEngine.Object * Set "Nitrox" as default style and fixed expression bodied members getting reverted * Added GetFixAllProvider override to UnitySkippedObjectLifetimeFixProvider CodeFixProvider types should override it by convention. * Added ?? analysis to UnitySkippedObjectLifetimeAnalyzer * Disabled resharper analysis of ?? for UnityEngine.Object * Set MSBuild property NitroxProject to false by default for all projects * Added LocalizationAnalyzer.cs This analyzer detects if localization key exists in the en.json (English localization). If it does not, then a warning is emitted. * Disabled localization analyzer if en.json is missing * Changed name of field invalidLocalizationKey to invalidLocalizationKeyRule * Added analyzer to prefer interpolated strings over string concat * Fixed typo in comment of StringUsageAnalyzer * Refactored unity object lifetime analyzer Added simple ITypeSymbol.IsType() that will properly resolve inheritance tree. * Used string interpolation in Nitrox.Analyzer project where possible * Added Increment source generator support to Nitrox.Analyzers * Improved accuracy of type symbol compare in UnitySkippedObjectLifetimeAnalyzer.cs Changed analyzer to get the type symbol of UnityEngine.Object from the compilation context at startup. * Changed debug logger to apply on any type * Added dynamic resolve for System.Text.Json based on runtime used * Removed dependency on System.Text.Json Visual Studio cannot find external libraries compiled with analyzers. See issue: dotnet/roslyn#41785 (comment) For now we regex parse the English localization JSON file. Co-authored-by: Jannify <[email protected]>
- Loading branch information
Showing
18 changed files
with
640 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.IO; | ||
using System.Text.RegularExpressions; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
|
||
namespace Nitrox.Analyzers.Diagnostics; | ||
|
||
/// <summary> | ||
/// Tests that requested localization keys exist in the English localization file. | ||
/// </summary> | ||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
public sealed class LocalizationAnalyzer : DiagnosticAnalyzer | ||
{ | ||
public const string INVALID_LOCALIZATION_KEY_DIAGNOSTIC_ID = $"{nameof(LocalizationAnalyzer)}001"; | ||
|
||
private const string NITROX_LOCALIZATION_PREFIX = "Nitrox_"; | ||
private static readonly string relativePathFromSolutionDirToEnglishLanguageFile = Path.Combine("Nitrox.Assets.Subnautica", "LanguageFiles", "en.json"); | ||
private static readonly Regex localizationParseRegex = new(@"^\s*""([^""]+)""\s*:\s*""([^""]+)""", RegexOptions.Compiled | RegexOptions.Multiline); | ||
|
||
private static readonly DiagnosticDescriptor invalidLocalizationKeyRule = new(INVALID_LOCALIZATION_KEY_DIAGNOSTIC_ID, | ||
"Tests localization usages are valid", | ||
"Localization key '{0}' does not exist in '{1}'", | ||
"Usage", | ||
DiagnosticSeverity.Warning, | ||
true, | ||
"Tests that requested localization keys exist in the English localization file"); | ||
|
||
/// <summary> | ||
/// Gets the list of rules of supported diagnostics. | ||
/// </summary> | ||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(invalidLocalizationKeyRule); | ||
|
||
/// <summary> | ||
/// Initializes the analyzer by registering on symbol occurrence in the targeted code. | ||
/// </summary> | ||
public override void Initialize(AnalysisContext context) | ||
{ | ||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
context.EnableConcurrentExecution(); | ||
|
||
context.RegisterCompilationStartAction(startContext => | ||
{ | ||
startContext.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.projectdir", out string projectDir); | ||
if (LocalizationHelper.Load(projectDir)) | ||
{ | ||
startContext.RegisterSyntaxNodeAction(AnalyzeStringNode, SyntaxKind.StringLiteralExpression); | ||
} | ||
}); | ||
} | ||
|
||
/// <summary> | ||
/// Analyzes string literals in code that are passed as argument to 'Language.main.Get'. | ||
/// </summary> | ||
private void AnalyzeStringNode(SyntaxNodeAnalysisContext context) | ||
{ | ||
LiteralExpressionSyntax expression = (LiteralExpressionSyntax)context.Node; | ||
if (expression.Parent is not ArgumentSyntax argument) | ||
{ | ||
return; | ||
} | ||
if (argument.Parent is not { Parent: InvocationExpressionSyntax invocation }) | ||
{ | ||
return; | ||
} | ||
if (context.SemanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol method) | ||
{ | ||
return; | ||
} | ||
if (method is not { ContainingType.Name: "Language", Name: "Get" }) | ||
{ | ||
return; | ||
} | ||
// Ignore language call for non-nitrox keys. | ||
string stringValue = expression.Token.ValueText; | ||
if (!stringValue.StartsWith(NITROX_LOCALIZATION_PREFIX, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
return; | ||
} | ||
if (LocalizationHelper.ContainsKey(stringValue)) | ||
{ | ||
return; | ||
} | ||
context.ReportDiagnostic(Diagnostic.Create(invalidLocalizationKeyRule, context.Node.GetLocation(), stringValue, LocalizationHelper.FileName)); | ||
} | ||
|
||
/// <summary> | ||
/// Wrapper API for synchronized access to the English localization file. | ||
/// </summary> | ||
private static class LocalizationHelper | ||
{ | ||
private static readonly object locker = new(); | ||
private static string EnglishLocalizationFileName { get; set; } = ""; | ||
private static ImmutableDictionary<string, string> EnglishLocalization { get; set; } = ImmutableDictionary<string, string>.Empty; | ||
|
||
public static bool IsEmpty | ||
{ | ||
get | ||
{ | ||
lock (locker) | ||
{ | ||
return EnglishLocalization.IsEmpty; | ||
} | ||
} | ||
} | ||
|
||
public static string FileName | ||
{ | ||
get | ||
{ | ||
lock (locker) | ||
{ | ||
return EnglishLocalizationFileName; | ||
} | ||
} | ||
} | ||
|
||
public static bool ContainsKey(string key) | ||
{ | ||
lock (locker) | ||
{ | ||
return EnglishLocalization.ContainsKey(key); | ||
} | ||
} | ||
|
||
public static bool Load(string projectDir) | ||
{ | ||
if (string.IsNullOrWhiteSpace(projectDir)) | ||
{ | ||
return false; | ||
} | ||
string solutionDir = Directory.GetParent(projectDir)?.Parent?.FullName; | ||
if (!Directory.Exists(solutionDir)) | ||
{ | ||
return false; | ||
} | ||
|
||
string enJson; | ||
lock (locker) | ||
{ | ||
EnglishLocalizationFileName = Path.Combine(solutionDir, relativePathFromSolutionDirToEnglishLanguageFile); | ||
if (!File.Exists(EnglishLocalizationFileName)) | ||
{ | ||
return false; | ||
} | ||
|
||
enJson = File.ReadAllText(EnglishLocalizationFileName); | ||
} | ||
// Parse localization JSON to dictionary for lookup. | ||
Dictionary<string, string> keyValue = new(); | ||
foreach (Match match in localizationParseRegex.Matches(enJson)) | ||
{ | ||
keyValue.Add(match.Groups[1].Value, match.Groups[2].Value); | ||
} | ||
lock (locker) | ||
{ | ||
EnglishLocalization = keyValue.ToImmutableDictionary(); | ||
} | ||
return true; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
using System.Collections.Immutable; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
|
||
namespace Nitrox.Analyzers.Diagnostics; | ||
|
||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
public sealed class StringUsageAnalyzer : DiagnosticAnalyzer | ||
{ | ||
public const string PREFER_INTERPOLATED_STRING_DIAGNOSTIC_ID = $"{nameof(StringUsageAnalyzer)}001"; | ||
|
||
private static readonly DiagnosticDescriptor preferInterpolatedStringRule = new(PREFER_INTERPOLATED_STRING_DIAGNOSTIC_ID, | ||
"Prefer interpolated string over string concat", | ||
"String concat can be turned into interpolated string", | ||
"Usage", | ||
DiagnosticSeverity.Warning, | ||
true, | ||
"Prefer interpolated string over concatenating strings"); | ||
|
||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(preferInterpolatedStringRule); | ||
|
||
public override void Initialize(AnalysisContext context) | ||
{ | ||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
context.EnableConcurrentExecution(); | ||
|
||
context.RegisterSyntaxNodeAction(AnalyzeAddNode, SyntaxKind.AddExpression); | ||
} | ||
|
||
private void AnalyzeAddNode(SyntaxNodeAnalysisContext context) | ||
{ | ||
bool IsPartOfStringConcat(SyntaxNode node) | ||
{ | ||
switch (node) | ||
{ | ||
case LiteralExpressionSyntax literal: | ||
return literal.IsKind(SyntaxKind.StringLiteralExpression); | ||
case InterpolatedStringExpressionSyntax: | ||
return true; | ||
case MemberAccessExpressionSyntax member: | ||
string memberType = context.SemanticModel.GetTypeInfo(member).ConvertedType?.Name; | ||
return memberType == "String"; | ||
case BinaryExpressionSyntax binary: | ||
// If one side is string-ish then the other side will get implicitly casted to string. | ||
return binary.IsKind(SyntaxKind.AddExpression) && (IsPartOfStringConcat(binary.Right) || IsPartOfStringConcat(binary.Left)); | ||
default: | ||
return false; | ||
} | ||
} | ||
|
||
static bool IsLeftMostNodeInConcat(SyntaxNode node) | ||
{ | ||
switch (node) | ||
{ | ||
case BinaryExpressionSyntax: | ||
case InterpolatedStringContentSyntax: | ||
return true; | ||
case ParenthesizedExpressionSyntax: | ||
return IsLeftMostNodeInConcat(node.Parent); | ||
} | ||
return false; | ||
} | ||
|
||
BinaryExpressionSyntax expression = (BinaryExpressionSyntax)context.Node; | ||
// Deduplicate warnings. Only left most '+' of the expression should be handled here. | ||
if (IsLeftMostNodeInConcat(expression.Parent)) | ||
{ | ||
return; | ||
} | ||
// Test if this should be interpolated. | ||
if (!IsPartOfStringConcat(expression.Left) && !IsPartOfStringConcat(expression.Right)) | ||
{ | ||
return; | ||
} | ||
|
||
context.ReportDiagnostic(Diagnostic.Create(preferInterpolatedStringRule, expression.GetLocation(), expression)); | ||
} | ||
} |
Oops, something went wrong.