From aa9c6cd6388515fac1bc65b62a70d2c5acedd6cc Mon Sep 17 00:00:00 2001 From: lice Date: Fri, 22 May 2026 11:39:42 +0200 Subject: [PATCH 1/4] Update analyzer deps / setup --- .../MN.L10n.Analyzer.Test.csproj | 2 +- .../MN.L10n.Analyzer.Vsix.csproj | 72 ------ .../source.extension.vsixmanifest | 23 -- .../MN.L10n.Analyzer/MN.L10n.Analyzer.csproj | 30 +-- .../MN.L10n.Analyzer/MNL10nAnalyzer.cs | 210 +++++++++--------- MN.L10n.sln | 6 - 6 files changed, 123 insertions(+), 220 deletions(-) delete mode 100644 MN.L10n.Analyzer/MN.L10n.Analyzer.Vsix/MN.L10n.Analyzer.Vsix.csproj delete mode 100644 MN.L10n.Analyzer/MN.L10n.Analyzer.Vsix/source.extension.vsixmanifest diff --git a/MN.L10n.Analyzer/MN.L10n.Analyzer.Test/MN.L10n.Analyzer.Test.csproj b/MN.L10n.Analyzer/MN.L10n.Analyzer.Test/MN.L10n.Analyzer.Test.csproj index 78ee7ec..2293206 100644 --- a/MN.L10n.Analyzer/MN.L10n.Analyzer.Test/MN.L10n.Analyzer.Test.csproj +++ b/MN.L10n.Analyzer/MN.L10n.Analyzer.Test/MN.L10n.Analyzer.Test.csproj @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/MN.L10n.Analyzer/MN.L10n.Analyzer.Vsix/MN.L10n.Analyzer.Vsix.csproj b/MN.L10n.Analyzer/MN.L10n.Analyzer.Vsix/MN.L10n.Analyzer.Vsix.csproj deleted file mode 100644 index e1cbacc..0000000 --- a/MN.L10n.Analyzer/MN.L10n.Analyzer.Vsix/MN.L10n.Analyzer.Vsix.csproj +++ /dev/null @@ -1,72 +0,0 @@ - - - - - 15.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - 14.0 - - - - - - Debug - AnyCPU - AnyCPU - 2.0 - {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - {0FBE481E-769A-4818-896E-3CD4580CA7A2} - Library - Properties - MN.L10n.Analyzer.Vsix - MN.L10n.Analyzer - v4.7.2 - false - false - false - false - false - false - Roslyn - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - Program - $(DevEnvDir)devenv.exe - /rootsuffix Roslyn - - - - .editorconfig - - - Designer - - - - - {C47AC9D6-C6AF-45A6-9596-3A63B48EEA9C} - MN.L10n.Analyzer - - - - - \ No newline at end of file diff --git a/MN.L10n.Analyzer/MN.L10n.Analyzer.Vsix/source.extension.vsixmanifest b/MN.L10n.Analyzer/MN.L10n.Analyzer.Vsix/source.extension.vsixmanifest deleted file mode 100644 index ef7e42f..0000000 --- a/MN.L10n.Analyzer/MN.L10n.Analyzer.Vsix/source.extension.vsixmanifest +++ /dev/null @@ -1,23 +0,0 @@ - - - - - MN.L10n.Analyzer - This is a sample diagnostic extension for the .NET Compiler Platform ("Roslyn"). - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/MN.L10n.Analyzer/MN.L10n.Analyzer/MN.L10n.Analyzer.csproj b/MN.L10n.Analyzer/MN.L10n.Analyzer/MN.L10n.Analyzer.csproj index 6ae1812..24c976c 100644 --- a/MN.L10n.Analyzer/MN.L10n.Analyzer/MN.L10n.Analyzer.csproj +++ b/MN.L10n.Analyzer/MN.L10n.Analyzer/MN.L10n.Analyzer.csproj @@ -2,10 +2,13 @@ netstandard2.0 - false True true true + enable + latest + true + false @@ -21,17 +24,15 @@ MN.L10n.Analyzer, analyzers true - 4.0.2 + 5.0.0-beta.1 MultiNet Interactive AB - - - x64 - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + @@ -39,14 +40,13 @@ - - - - - - + + + + + diff --git a/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nAnalyzer.cs b/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nAnalyzer.cs index 01e60d7..e3928a3 100644 --- a/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nAnalyzer.cs +++ b/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nAnalyzer.cs @@ -2,143 +2,147 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text.RegularExpressions; -namespace MN.L10n.Analyzer +namespace MN.L10n.Analyzer; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class MNL10nAnalyzer : DiagnosticAnalyzer { - [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class MNL10nAnalyzer : DiagnosticAnalyzer + public static readonly DiagnosticDescriptor NoParamRule = new DiagnosticDescriptor("MN0001", "Missing arguments", "Need to send variables to '{0}'", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor MemberAccessorRule = new DiagnosticDescriptor("MN0002", "Input is not a known string", "L10n can only evaluate string literals when finding used phrases. If the phrase is known you can ignore this, but you should only use L10n with known strings when possible.", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor NoWhitespaceAtStartOrEndRule = new DiagnosticDescriptor("MN0003", "String starts/ends with whitespace", "The string cannot start or end with whitespaces", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor NoEmptyStringsEndRule = new DiagnosticDescriptor("MN0004", "Input is an empty string", "The string cannot start or end with whitespace", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor NoStringInterpolationRule = new DiagnosticDescriptor("MN0005", "Interpolated string used", "L10n can only evaluate string literals when finding used phrases. Never use string interpolation with L10n. It is not supported yet.", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor NoStringConcatRule = new DiagnosticDescriptor("MN0006", "String concatenation used", "L10n can only evaluate a single string literal when finding used phrases. Never use string concatenation with L10n. It is not supported yet.", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor ArgumentsNotAnClassOrNullRule = new DiagnosticDescriptor("MN0007", "Invalid type for keywords", "L10n requires a class or anonymous type (or explicitly null) for keywords", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor MissingKeywordReplacementObjectRule = new DiagnosticDescriptor("MN0008", "Missing object for keyword replacement", "L10n requires a class or anonymous type (or explicitly null) for keywords", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor MissingKeywordsInReplacementObjectRule = new DiagnosticDescriptor("MN0009", "Missing property for keyword replacement", "L10n is missing '{0}' in the object for keywords", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics { - public static readonly DiagnosticDescriptor NoParamRule = new DiagnosticDescriptor("MN0001", "Missing arguments", "Need to send variables to '{0}'", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor MemberAccessorRule = new DiagnosticDescriptor("MN0002", "Input is not a known string", "L10n can only evaluate string literals when finding used phrases. If the phrase is known you can ignore this, but you should only use L10n with known strings when possible.", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor NoWhitespaceAtStartOrEndRule = new DiagnosticDescriptor("MN0003", "String starts/ends with whitespace", "The string cannot start or end with whitespaces", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor NoEmptyStringsEndRule = new DiagnosticDescriptor("MN0004", "Input is an empty string", "The string cannot start or end with whitespace", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor NoStringInterpolationRule = new DiagnosticDescriptor("MN0005", "Interpolated string used", "L10n can only evaluate string literals when finding used phrases. Never use string interpolation with L10n. It is not supported yet.", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor NoStringConcatRule = new DiagnosticDescriptor("MN0006", "String concatenation used", "L10n can only evaluate a single string literal when finding used phrases. Never use string concatenation with L10n. It is not supported yet.", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor ArgumentsNotAnClassOrNullRule = new DiagnosticDescriptor("MN0007", "Invalid type for keywords", "L10n requires a class or anonymous type (or explicitly null) for keywords", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor MissingKeywordReplacementObjectRule = new DiagnosticDescriptor("MN0008", "Missing object for keyword replacement", "L10n requires a class or anonymous type (or explicitly null) for keywords", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor MissingKeywordsInReplacementObjectRule = new DiagnosticDescriptor("MN0009", "Missing property for keyword replacement", "L10n is missing '{0}' in the object for keywords", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); - - public override ImmutableArray SupportedDiagnostics + get { - get - { - return ImmutableArray.Create( - NoParamRule, - MemberAccessorRule, - NoWhitespaceAtStartOrEndRule, - NoEmptyStringsEndRule, - NoStringInterpolationRule, - NoStringConcatRule, - ArgumentsNotAnClassOrNullRule, - MissingKeywordReplacementObjectRule, - MissingKeywordsInReplacementObjectRule - ); - } + return ImmutableArray.Create( + NoParamRule, + MemberAccessorRule, + NoWhitespaceAtStartOrEndRule, + NoEmptyStringsEndRule, + NoStringInterpolationRule, + NoStringConcatRule, + ArgumentsNotAnClassOrNullRule, + MissingKeywordReplacementObjectRule, + MissingKeywordsInReplacementObjectRule + ); } + } - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.InvocationExpression); - } + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.InvocationExpression); + } - private static readonly string[] validIdentifiers = new[] - { - "_s", "_m", - "L10n._s", "L10n._m", - }; + private static readonly string[] validIdentifiers = new[] + { + "_s", "_m", + "L10n._s", "L10n._m", + }; - static Regex r = new Regex(@"(\$(?:[a-zA-Z0-9_]+?)\$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + static Regex r = new Regex(@"(\$(?:[a-zA-Z0-9_]+?)\$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public static HashSet L10nParameters(string input) => new HashSet(r.Matches(input).Cast().Select(m => m.Groups[1].Value)); + public static HashSet L10nParameters(string input) => new HashSet(r.Matches(input).Cast().Select(m => m.Groups[1].Value)); - private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext obj) - { - if (!(obj.Node is InvocationExpressionSyntax ies)) return; + private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext obj) + { + if (!(obj.Node is InvocationExpressionSyntax ies)) return; - var identifier = (ies.Expression as IdentifierNameSyntax)?.Identifier.Text - ?? (ies.Expression as MemberAccessExpressionSyntax)?.ToString(); + var identifier = (ies.Expression as IdentifierNameSyntax)?.Identifier.Text + ?? (ies.Expression as MemberAccessExpressionSyntax)?.ToString(); - if (!validIdentifiers.Contains(identifier)) return; + if (!validIdentifiers.Contains(identifier)) return; - HashSet l10nParameters = null; + HashSet? l10nParameters = null; - var arguments = ies.ArgumentList.Arguments; - if (arguments.Count == 0) - obj.ReportDiagnostic(Diagnostic.Create(NoParamRule, obj.Node.GetLocation())); - else + var arguments = ies.ArgumentList.Arguments; + if (arguments.Count == 0) + obj.ReportDiagnostic(Diagnostic.Create(NoParamRule, obj.Node.GetLocation())); + else + { + var supposedString = arguments.First(); + switch (supposedString.Expression) { - var supposedString = arguments.First(); - switch (supposedString.Expression) - { - case BinaryExpressionSyntax binaryExpression: - obj.ReportDiagnostic(Diagnostic.Create(NoStringConcatRule, obj.Node.GetLocation())); - break; - case MemberAccessExpressionSyntax memberAccess: - case InvocationExpressionSyntax invocationExpression: - obj.ReportDiagnostic(Diagnostic.Create(MemberAccessorRule, obj.Node.GetLocation())); - break; - case InterpolatedStringExpressionSyntax interpolatedString: - obj.ReportDiagnostic(Diagnostic.Create(NoStringInterpolationRule, obj.Node.GetLocation())); - break; - case LiteralExpressionSyntax literalExpression: - var text = literalExpression.Token.ValueText; - if (string.IsNullOrWhiteSpace(text)) + case BinaryExpressionSyntax binaryExpression: + obj.ReportDiagnostic(Diagnostic.Create(NoStringConcatRule, obj.Node.GetLocation())); + break; + case MemberAccessExpressionSyntax memberAccess: + case InvocationExpressionSyntax invocationExpression: + obj.ReportDiagnostic(Diagnostic.Create(MemberAccessorRule, obj.Node.GetLocation())); + break; + case InterpolatedStringExpressionSyntax interpolatedString: + obj.ReportDiagnostic(Diagnostic.Create(NoStringInterpolationRule, obj.Node.GetLocation())); + break; + case LiteralExpressionSyntax literalExpression: + var text = literalExpression.Token.ValueText; + if (string.IsNullOrWhiteSpace(text)) + { + obj.ReportDiagnostic(Diagnostic.Create(NoEmptyStringsEndRule, obj.Node.GetLocation())); + } + else + { + if (char.IsWhiteSpace(text.First()) || char.IsWhiteSpace(text.Last())) { - obj.ReportDiagnostic(Diagnostic.Create(NoEmptyStringsEndRule, obj.Node.GetLocation())); + obj.ReportDiagnostic(Diagnostic.Create(NoWhitespaceAtStartOrEndRule, obj.Node.GetLocation())); } - else + } + + if (text.Contains("$")) + { + l10nParameters = L10nParameters(text); + + if (arguments.Count == 1) { - if (char.IsWhiteSpace(text.First()) || char.IsWhiteSpace(text.Last())) - { - obj.ReportDiagnostic(Diagnostic.Create(NoWhitespaceAtStartOrEndRule, obj.Node.GetLocation())); - } + obj.ReportDiagnostic(Diagnostic.Create(MissingKeywordReplacementObjectRule, obj.Node.GetLocation())); } + } - if (text.Contains("$")) - { - l10nParameters = L10nParameters(text); + break; + } - if (arguments.Count == 1) - { - obj.ReportDiagnostic(Diagnostic.Create(MissingKeywordReplacementObjectRule, obj.Node.GetLocation())); - } - } + if (arguments.Count > 1) + { + var supposedArgument = arguments[1]; - break; + if (!(supposedArgument.Expression is ObjectCreationExpressionSyntax || supposedArgument.Expression is AnonymousObjectCreationExpressionSyntax || supposedArgument.Expression.RawKind == (int)SyntaxKind.NullLiteralExpression)) + { + obj.ReportDiagnostic(Diagnostic.Create(ArgumentsNotAnClassOrNullRule, obj.Node.GetLocation())); } - - if (arguments.Count > 1) + else { - var supposedArgument = arguments[1]; - - if (!(supposedArgument.Expression is ObjectCreationExpressionSyntax || supposedArgument.Expression is AnonymousObjectCreationExpressionSyntax || supposedArgument.Expression.RawKind == (int)SyntaxKind.NullLiteralExpression)) + if (l10nParameters == null || l10nParameters.Count == 0) { - obj.ReportDiagnostic(Diagnostic.Create(ArgumentsNotAnClassOrNullRule, obj.Node.GetLocation())); + return; } - else - { - if (l10nParameters == null || l10nParameters.Count == 0) - { - return; - } - var argumentAsObject = obj.SemanticModel.GetSymbolInfo(supposedArgument.Expression).Symbol; + var argumentAsObject = obj.SemanticModel.GetSymbolInfo(supposedArgument.Expression).Symbol; - var missingParameters = l10nParameters.Except(argumentAsObject.ContainingType.MemberNames.Select(p => $"${p}$")); + if (argumentAsObject == null) + { + return; + } - if (missingParameters.Any()) + var missingParameters = l10nParameters.Except(argumentAsObject.ContainingType.MemberNames.Select(p => $"${p}$")) + .ToArray(); + + if (missingParameters.Any()) + { + foreach (var p in missingParameters) { - foreach (var p in missingParameters) - { - obj.ReportDiagnostic(Diagnostic.Create(MissingKeywordsInReplacementObjectRule, obj.Node.GetLocation(), p)); - } + obj.ReportDiagnostic(Diagnostic.Create(MissingKeywordsInReplacementObjectRule, obj.Node.GetLocation(), p)); } } } diff --git a/MN.L10n.sln b/MN.L10n.sln index 34789cb..08f6f07 100644 --- a/MN.L10n.sln +++ b/MN.L10n.sln @@ -33,8 +33,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MN.L10n.Analyzer", "MN.L10n EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MN.L10n.Analyzer.Test", "MN.L10n.Analyzer\MN.L10n.Analyzer.Test\MN.L10n.Analyzer.Test.csproj", "{45A4B5FB-A04C-40E0-BA01-250E2E6C308B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MN.L10n.Analyzer.Vsix", "MN.L10n.Analyzer\MN.L10n.Analyzer.Vsix\MN.L10n.Analyzer.Vsix.csproj", "{0FBE481E-769A-4818-896E-3CD4580CA7A2}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MN.L10n.SystemWeb", "MN.L10n.SystemWeb\MN.L10n.SystemWeb.csproj", "{8096E105-0A0B-446F-89DE-599312C7E25E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MN.L10n.Tests", "MN.L10n.Tests\MN.L10n.Tests.csproj", "{80D8F7EE-7A63-41DB-9FD4-D0D4D1D69F21}" @@ -85,10 +83,6 @@ Global {45A4B5FB-A04C-40E0-BA01-250E2E6C308B}.Debug|Any CPU.Build.0 = Debug|Any CPU {45A4B5FB-A04C-40E0-BA01-250E2E6C308B}.Release|Any CPU.ActiveCfg = Release|Any CPU {45A4B5FB-A04C-40E0-BA01-250E2E6C308B}.Release|Any CPU.Build.0 = Release|Any CPU - {0FBE481E-769A-4818-896E-3CD4580CA7A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0FBE481E-769A-4818-896E-3CD4580CA7A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0FBE481E-769A-4818-896E-3CD4580CA7A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0FBE481E-769A-4818-896E-3CD4580CA7A2}.Release|Any CPU.Build.0 = Release|Any CPU {8096E105-0A0B-446F-89DE-599312C7E25E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8096E105-0A0B-446F-89DE-599312C7E25E}.Debug|Any CPU.Build.0 = Debug|Any CPU {8096E105-0A0B-446F-89DE-599312C7E25E}.Release|Any CPU.ActiveCfg = Release|Any CPU From 44399e4dba645a9fc4f20dc32b4c5692a833a32c Mon Sep 17 00:00:00 2001 From: lice Date: Fri, 22 May 2026 13:56:40 +0200 Subject: [PATCH 2/4] Removed redundant NoParam rule. The rule served no purpose, since _s requires at least one parameter to compile, we do not need an analyzer to validate that. --- .../MN.L10n.Analyzer/AnalyzerReleases.Shipped.md | 1 - MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nAnalyzer.cs | 6 +----- MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nCodeFixProvider.cs | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/MN.L10n.Analyzer/MN.L10n.Analyzer/AnalyzerReleases.Shipped.md b/MN.L10n.Analyzer/MN.L10n.Analyzer/AnalyzerReleases.Shipped.md index ff90a90..a84311f 100644 --- a/MN.L10n.Analyzer/MN.L10n.Analyzer/AnalyzerReleases.Shipped.md +++ b/MN.L10n.Analyzer/MN.L10n.Analyzer/AnalyzerReleases.Shipped.md @@ -7,7 +7,6 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- -MN0001 | L10n | Error | MNL10nAnalyzer MN0002 | L10n | Error | MNL10nAnalyzer MN0003 | L10n | Error | MNL10nAnalyzer MN0004 | L10n | Error | MNL10nAnalyzer diff --git a/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nAnalyzer.cs b/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nAnalyzer.cs index e3928a3..6147809 100644 --- a/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nAnalyzer.cs +++ b/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nAnalyzer.cs @@ -12,7 +12,6 @@ namespace MN.L10n.Analyzer; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class MNL10nAnalyzer : DiagnosticAnalyzer { - public static readonly DiagnosticDescriptor NoParamRule = new DiagnosticDescriptor("MN0001", "Missing arguments", "Need to send variables to '{0}'", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); public static readonly DiagnosticDescriptor MemberAccessorRule = new DiagnosticDescriptor("MN0002", "Input is not a known string", "L10n can only evaluate string literals when finding used phrases. If the phrase is known you can ignore this, but you should only use L10n with known strings when possible.", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); public static readonly DiagnosticDescriptor NoWhitespaceAtStartOrEndRule = new DiagnosticDescriptor("MN0003", "String starts/ends with whitespace", "The string cannot start or end with whitespaces", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); public static readonly DiagnosticDescriptor NoEmptyStringsEndRule = new DiagnosticDescriptor("MN0004", "Input is an empty string", "The string cannot start or end with whitespace", "L10n", DiagnosticSeverity.Error, isEnabledByDefault: true); @@ -27,7 +26,6 @@ public override ImmutableArray SupportedDiagnostics get { return ImmutableArray.Create( - NoParamRule, MemberAccessorRule, NoWhitespaceAtStartOrEndRule, NoEmptyStringsEndRule, @@ -69,9 +67,7 @@ private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext obj) HashSet? l10nParameters = null; var arguments = ies.ArgumentList.Arguments; - if (arguments.Count == 0) - obj.ReportDiagnostic(Diagnostic.Create(NoParamRule, obj.Node.GetLocation())); - else + if (arguments.Count > 0) { var supposedString = arguments.First(); switch (supposedString.Expression) diff --git a/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nCodeFixProvider.cs b/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nCodeFixProvider.cs index 45eb5fa..ebb0c28 100644 --- a/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nCodeFixProvider.cs +++ b/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nCodeFixProvider.cs @@ -14,7 +14,6 @@ public class MNL10nCodeFixProvider : CodeFixProvider public sealed override ImmutableArray FixableDiagnosticIds { get { return ImmutableArray.Create( - MNL10nAnalyzer.NoParamRule.Id, MNL10nAnalyzer.MemberAccessorRule.Id, MNL10nAnalyzer.NoEmptyStringsEndRule.Id, MNL10nAnalyzer.NoWhitespaceAtStartOrEndRule.Id, From feb426f75f046bb1e05999cf33c33a13d58c1910 Mon Sep 17 00:00:00 2001 From: lice Date: Tue, 2 Jun 2026 09:58:17 +0200 Subject: [PATCH 3/4] Fixed additional case the Analyzer was missing --- .../MNL10nAnalyzerUnitTests.cs | 26 +++++++++++++++++++ .../MN.L10n.Analyzer/MNL10nAnalyzer.cs | 8 ++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/MN.L10n.Analyzer/MN.L10n.Analyzer.Test/MNL10nAnalyzerUnitTests.cs b/MN.L10n.Analyzer/MN.L10n.Analyzer.Test/MNL10nAnalyzerUnitTests.cs index 566f51b..a65a99e 100644 --- a/MN.L10n.Analyzer/MN.L10n.Analyzer.Test/MNL10nAnalyzerUnitTests.cs +++ b/MN.L10n.Analyzer/MN.L10n.Analyzer.Test/MNL10nAnalyzerUnitTests.cs @@ -416,6 +416,32 @@ Vänligen bekräfta ditt intresse genom att klicka på länken nedan. VerifyCSharpDiagnostic(test, expectations.ToArray()); } + [Fact] + public void Test_MN0009_MissingKeywordsWhenArgumentIsNull() + { + var test = @" + namespace ConsoleApplication1 + { + class TypeName + { + public void Main() { + _s(""Testing $someParameter$"", null); + } + } + }"; + + VerifyCSharpDiagnostic(test, new DiagnosticResult + { + Id = "MN0009", + Message = "L10n is missing '$someParameter$' in the object for keywords", + Severity = DiagnosticSeverity.Error, + Locations = + [ + new DiagnosticResultLocation("Test0.cs", 7, 6) + ] + }); + } + protected override CodeFixProvider GetCSharpCodeFixProvider() { return new MNL10nCodeFixProvider(); diff --git a/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nAnalyzer.cs b/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nAnalyzer.cs index 6147809..46f7b9d 100644 --- a/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nAnalyzer.cs +++ b/MN.L10n.Analyzer/MN.L10n.Analyzer/MNL10nAnalyzer.cs @@ -126,12 +126,8 @@ private void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext obj) var argumentAsObject = obj.SemanticModel.GetSymbolInfo(supposedArgument.Expression).Symbol; - if (argumentAsObject == null) - { - return; - } - - var missingParameters = l10nParameters.Except(argumentAsObject.ContainingType.MemberNames.Select(p => $"${p}$")) + var missingParameters = argumentAsObject == null ? l10nParameters.ToArray() : + l10nParameters.Except(argumentAsObject.ContainingType.MemberNames.Select(p => $"${p}$")) .ToArray(); if (missingParameters.Any()) From 8e69dff6fe8992f9eca9730d69b82a9d20fa4e22 Mon Sep 17 00:00:00 2001 From: lice Date: Wed, 3 Jun 2026 09:25:05 +0200 Subject: [PATCH 4/4] Remove beta tag --- MN.L10n.Analyzer/MN.L10n.Analyzer/MN.L10n.Analyzer.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MN.L10n.Analyzer/MN.L10n.Analyzer/MN.L10n.Analyzer.csproj b/MN.L10n.Analyzer/MN.L10n.Analyzer/MN.L10n.Analyzer.csproj index 24c976c..9aa6eaa 100644 --- a/MN.L10n.Analyzer/MN.L10n.Analyzer/MN.L10n.Analyzer.csproj +++ b/MN.L10n.Analyzer/MN.L10n.Analyzer/MN.L10n.Analyzer.csproj @@ -24,7 +24,7 @@ MN.L10n.Analyzer, analyzers true - 5.0.0-beta.1 + 5.0.0 MultiNet Interactive AB