diff --git a/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs b/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs new file mode 100644 index 00000000000..f381aa31ec0 --- /dev/null +++ b/Flow.Launcher.Analyzers/AnalyzerDiagnostics.cs @@ -0,0 +1,52 @@ +using Microsoft.CodeAnalysis; + +namespace Flow.Launcher.Analyzers +{ + public static class AnalyzerDiagnostics + { + public static readonly DiagnosticDescriptor OldLocalizationApiUsed = new DiagnosticDescriptor( + "FLAN0001", + "Old localization API used", + "Use `Localize.{0}({1})` instead", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ContextIsAField = new DiagnosticDescriptor( + "FLAN0002", + "Plugin context is a field", + "Plugin context must be at least internal static property", + "Localization", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ContextIsNotStatic = new DiagnosticDescriptor( + "FLAN0003", + "Plugin context is not static", + "Plugin context must be at least internal static property", + "Localization", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ContextAccessIsTooRestrictive = new DiagnosticDescriptor( + "FLAN0004", + "Plugin context property access modifier is too restrictive", + "Plugin context property must be at least internal static property", + "Localization", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ContextIsNotDeclared = new DiagnosticDescriptor( + "FLAN0005", + "Plugin context is not declared", + "Plugin context must be at least internal static property of type `PluginInitContext`", + "Localization", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + } +} diff --git a/Flow.Launcher.Analyzers/AnalyzerReleases.Shipped.md b/Flow.Launcher.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000000..5ccc9f037f6 --- /dev/null +++ b/Flow.Launcher.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,2 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/Flow.Launcher.Analyzers/AnalyzerReleases.Unshipped.md b/Flow.Launcher.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000000..d5f177c6c1a --- /dev/null +++ b/Flow.Launcher.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1,11 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +FLAN0001 | Localization | Warning | FLAN0001_OldLocalizationApiUsed +FLAN0002 | Localization | Error | FLAN0002_ContextIsAField +FLAN0003 | Localization | Error | FLAN0003_ContextIsNotStatic +FLAN0004 | Localization | Error | FLAN0004_ContextAccessIsTooRestrictive +FLAN0005 | Localization | Error | FLAN0005_ContextIsNotDeclared diff --git a/Flow.Launcher.Analyzers/Flow.Launcher.Analyzers.csproj b/Flow.Launcher.Analyzers/Flow.Launcher.Analyzers.csproj new file mode 100644 index 00000000000..231d8d1afdd --- /dev/null +++ b/Flow.Launcher.Analyzers/Flow.Launcher.Analyzers.csproj @@ -0,0 +1,18 @@ + + + + 1.0.0 + netstandard2.0 + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzer.cs b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzer.cs new file mode 100644 index 00000000000..eede68bfe07 --- /dev/null +++ b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzer.cs @@ -0,0 +1,94 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Flow.Launcher.Analyzers.Localize +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ContextAvailabilityAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + AnalyzerDiagnostics.ContextIsAField, + AnalyzerDiagnostics.ContextIsNotStatic, + AnalyzerDiagnostics.ContextAccessIsTooRestrictive, + AnalyzerDiagnostics.ContextIsNotDeclared + ); + + private const string PluginContextTypeName = "PluginInitContext"; + + private const string PluginInterfaceName = "IPluginI18n"; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration); + } + + private static void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + var semanticModel = context.SemanticModel; + var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration); + + if (!IsPluginEntryClass(classSymbol)) return; + + var contextProperty = classDeclaration.Members.OfType() + .Select(p => semanticModel.GetDeclaredSymbol(p)) + .FirstOrDefault(p => p?.Type.Name is PluginContextTypeName); + + if (contextProperty != null) + { + if (!contextProperty.IsStatic) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerDiagnostics.ContextIsNotStatic, + contextProperty.DeclaringSyntaxReferences[0].GetSyntax().GetLocation() + )); + return; + } + + if (contextProperty.DeclaredAccessibility is Accessibility.Private || contextProperty.DeclaredAccessibility is Accessibility.Protected) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerDiagnostics.ContextAccessIsTooRestrictive, + contextProperty.DeclaringSyntaxReferences[0].GetSyntax().GetLocation() + )); + return; + } + + return; + } + + var fieldDeclaration = classDeclaration.Members + .OfType() + .SelectMany(f => f.Declaration.Variables) + .Select(f => semanticModel.GetDeclaredSymbol(f)) + .FirstOrDefault(f => f is IFieldSymbol fs && fs.Type.Name is PluginContextTypeName); + var parentSyntax = fieldDeclaration + ?.DeclaringSyntaxReferences[0] + .GetSyntax() + .FirstAncestorOrSelf(); + + if (parentSyntax != null) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerDiagnostics.ContextIsAField, + parentSyntax.GetLocation() + )); + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerDiagnostics.ContextIsNotDeclared, + classDeclaration.Identifier.GetLocation() + )); + } + + private static bool IsPluginEntryClass(INamedTypeSymbol namedTypeSymbol) => + namedTypeSymbol?.Interfaces.Any(i => i.Name == PluginInterfaceName) ?? false; + } +} diff --git a/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs new file mode 100644 index 00000000000..9b798018a21 --- /dev/null +++ b/Flow.Launcher.Analyzers/Localize/ContextAvailabilityAnalyzerCodeFixProvider.cs @@ -0,0 +1,188 @@ +using System.Collections.Immutable; +using System.Composition; +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.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Text; + +namespace Flow.Launcher.Analyzers.Localize +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ContextAvailabilityAnalyzerCodeFixProvider)), Shared] + public class ContextAvailabilityAnalyzerCodeFixProvider : CodeFixProvider + { + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( + AnalyzerDiagnostics.ContextIsAField.Id, + AnalyzerDiagnostics.ContextIsNotStatic.Id, + AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id, + AnalyzerDiagnostics.ContextIsNotDeclared.Id + ); + + public sealed override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + if (diagnostic.Id == AnalyzerDiagnostics.ContextIsAField.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Replace with static property", + createChangedDocument: _ => Task.FromResult(FixContextIsAFieldError(context, root, diagnosticSpan)), + equivalenceKey: AnalyzerDiagnostics.ContextIsAField.Id + ), + diagnostic + ); + } + else if (diagnostic.Id == AnalyzerDiagnostics.ContextIsNotStatic.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Make static", + createChangedDocument: _ => Task.FromResult(FixContextIsNotStaticError(context, root, diagnosticSpan)), + equivalenceKey: AnalyzerDiagnostics.ContextIsNotStatic.Id + ), + diagnostic + ); + } + else if (diagnostic.Id == AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Make internal", + createChangedDocument: _ => Task.FromResult(FixContextIsTooRestricted(context, root, diagnosticSpan)), + equivalenceKey: AnalyzerDiagnostics.ContextAccessIsTooRestrictive.Id + ), + diagnostic + ); + } + else if (diagnostic.Id == AnalyzerDiagnostics.ContextIsNotDeclared.Id) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Declare context property", + createChangedDocument: _ => Task.FromResult(FixContextNotDeclared(context, root, diagnosticSpan)), + equivalenceKey: AnalyzerDiagnostics.ContextIsNotDeclared.Id + ), + diagnostic + ); + } + } + + private static MemberDeclarationSyntax GetStaticContextPropertyDeclaration(string propertyName = "Context") => + SyntaxFactory.ParseMemberDeclaration( + $"internal static PluginInitContext {propertyName} {{ get; private set; }} = null!;" + ); + + private static Document GetFormattedDocument(CodeFixContext context, SyntaxNode root) + { + var formattedRoot = Formatter.Format( + root, + Formatter.Annotation, + context.Document.Project.Solution.Workspace + ); + + return context.Document.WithSyntaxRoot(formattedRoot); + } + + private static Document FixContextNotDeclared(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) + { + var classDeclaration = GetDeclarationSyntax(root, diagnosticSpan); + if (classDeclaration?.BaseList is null) return context.Document; + + var newPropertyDeclaration = GetStaticContextPropertyDeclaration(); + if (newPropertyDeclaration is null) return context.Document; + + var annotatedNewPropertyDeclaration = newPropertyDeclaration + .WithLeadingTrivia(SyntaxFactory.ElasticLineFeed) + .WithTrailingTrivia(SyntaxFactory.ElasticLineFeed) + .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation); + + var newMembers = classDeclaration.Members.Insert(0, annotatedNewPropertyDeclaration); + var newClassDeclaration = classDeclaration.WithMembers(newMembers); + + var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration); + + return GetFormattedDocument(context, newRoot); + } + + private static Document FixContextIsNotStaticError(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) + { + var propertyDeclaration = GetDeclarationSyntax(root, diagnosticSpan); + if (propertyDeclaration is null) return context.Document; + + var newPropertyDeclaration = FixRestrictivePropertyModifiers(propertyDeclaration).AddModifiers(SyntaxFactory.Token(SyntaxKind.StaticKeyword)); + + var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration); + return context.Document.WithSyntaxRoot(newRoot); + } + + private static Document FixContextIsTooRestricted(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) + { + var propertyDeclaration = GetDeclarationSyntax(root, diagnosticSpan); + if (propertyDeclaration is null) return context.Document; + + var newPropertyDeclaration = FixRestrictivePropertyModifiers(propertyDeclaration); + + var newRoot = root.ReplaceNode(propertyDeclaration, newPropertyDeclaration); + return context.Document.WithSyntaxRoot(newRoot); + } + + private static Document FixContextIsAFieldError(CodeFixContext context, SyntaxNode root, TextSpan diagnosticSpan) { + var fieldDeclaration = GetDeclarationSyntax(root, diagnosticSpan); + if (fieldDeclaration is null) return context.Document; + + var field = fieldDeclaration.Declaration.Variables.First(); + var fieldIdentifier = field.Identifier.ToString(); + + var propertyDeclaration = GetStaticContextPropertyDeclaration(fieldIdentifier); + if (propertyDeclaration is null) return context.Document; + + var annotatedNewPropertyDeclaration = propertyDeclaration + .WithTriviaFrom(fieldDeclaration) + .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation); + + var newRoot = root.ReplaceNode(fieldDeclaration, annotatedNewPropertyDeclaration); + + return GetFormattedDocument(context, newRoot); + } + + private static PropertyDeclarationSyntax FixRestrictivePropertyModifiers(PropertyDeclarationSyntax propertyDeclaration) + { + var newModifiers = SyntaxFactory.TokenList(); + foreach (var modifier in propertyDeclaration.Modifiers) + { + if (modifier.IsKind(SyntaxKind.PrivateKeyword) || modifier.IsKind(SyntaxKind.ProtectedKeyword)) + { + newModifiers = newModifiers.Add(SyntaxFactory.Token(SyntaxKind.InternalKeyword)); + } + else + { + newModifiers = newModifiers.Add(modifier); + } + } + + return propertyDeclaration.WithModifiers(newModifiers); + } + + private static T GetDeclarationSyntax(SyntaxNode root, TextSpan diagnosticSpan) where T : SyntaxNode => + root + .FindToken(diagnosticSpan.Start) + .Parent + ?.AncestorsAndSelf() + .OfType() + .First(); + } +} diff --git a/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzer.cs b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzer.cs new file mode 100644 index 00000000000..25f0f6ef36b --- /dev/null +++ b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzer.cs @@ -0,0 +1,100 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Flow.Launcher.Analyzers.Localize +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class OldGetTranslateAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(AnalyzerDiagnostics.OldLocalizationApiUsed); + + private static readonly string[] oldLocalizationClasses = { "IPublicAPI", "Internationalization" }; + private const string OldLocalizationMethodName = "GetTranslation"; + + private const string StringFormatMethodName = "Format"; + private const string StringFormatTypeName = "string"; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var invocationExpr = (InvocationExpressionSyntax)context.Node; + var semanticModel = context.SemanticModel; + var symbolInfo = semanticModel.GetSymbolInfo(invocationExpr); + + if (!(symbolInfo.Symbol is IMethodSymbol methodSymbol)) return; + + if (IsFormatStringCall(methodSymbol) && + GetFirstArgumentInvocationExpression(invocationExpr) is InvocationExpressionSyntax innerInvocationExpr) + { + if (!IsTranslateCall(semanticModel.GetSymbolInfo(innerInvocationExpr)) || + !(GetFirstArgumentStringValue(innerInvocationExpr) is string translationKey)) + return; + + var diagnostic = Diagnostic.Create( + AnalyzerDiagnostics.OldLocalizationApiUsed, + invocationExpr.GetLocation(), + translationKey, + GetInvocationArguments(invocationExpr) + ); + context.ReportDiagnostic(diagnostic); + } + else if (IsTranslateCall(methodSymbol) && GetFirstArgumentStringValue(invocationExpr) is string translationKey) + { + if (IsParentFormatStringCall(semanticModel, invocationExpr)) return; + + var diagnostic = Diagnostic.Create( + AnalyzerDiagnostics.OldLocalizationApiUsed, + invocationExpr.GetLocation(), + translationKey, + string.Empty + ); + context.ReportDiagnostic(diagnostic); + } + } + + private static string GetInvocationArguments(InvocationExpressionSyntax invocationExpr) => + string.Join(", ", invocationExpr.ArgumentList.Arguments.Skip(1)); + + private static bool IsParentFormatStringCall(SemanticModel semanticModel, SyntaxNode syntaxNode) => + syntaxNode is InvocationExpressionSyntax invocationExpressionSyntax && + invocationExpressionSyntax.Parent?.Parent?.Parent is SyntaxNode parent && + IsFormatStringCall(semanticModel?.GetSymbolInfo(parent)); + + private static bool IsFormatStringCall(SymbolInfo? symbolInfo) => + symbolInfo is SymbolInfo info && IsFormatStringCall(info.Symbol as IMethodSymbol); + + private static bool IsFormatStringCall(IMethodSymbol methodSymbol) => + methodSymbol?.Name is StringFormatMethodName && + methodSymbol.ContainingType.ToDisplayString() is StringFormatTypeName; + + private static InvocationExpressionSyntax GetFirstArgumentInvocationExpression(InvocationExpressionSyntax invocationExpr) => + invocationExpr.ArgumentList.Arguments.FirstOrDefault()?.Expression as InvocationExpressionSyntax; + + private static bool IsTranslateCall(SymbolInfo symbolInfo) => + symbolInfo.Symbol is IMethodSymbol innerMethodSymbol && + innerMethodSymbol.Name is OldLocalizationMethodName && + oldLocalizationClasses.Contains(innerMethodSymbol.ContainingType.Name); + + private static bool IsTranslateCall(IMethodSymbol methodSymbol) => + methodSymbol?.Name is OldLocalizationMethodName && + oldLocalizationClasses.Contains(methodSymbol.ContainingType.Name); + + private static string GetFirstArgumentStringValue(InvocationExpressionSyntax invocationExpr) + { + if (invocationExpr.ArgumentList.Arguments.FirstOrDefault()?.Expression is LiteralExpressionSyntax syntax) + return syntax.Token.ValueText; + return null; + } + } +} diff --git a/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs new file mode 100644 index 00000000000..87fb37a8355 --- /dev/null +++ b/Flow.Launcher.Analyzers/Localize/OldGetTranslateAnalyzerCodeFixProvider.cs @@ -0,0 +1,114 @@ +using System.Collections.Immutable; +using System.Composition; +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.CSharp.Syntax; + +namespace Flow.Launcher.Analyzers.Localize +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OldGetTranslateAnalyzerCodeFixProvider)), Shared] + public class OldGetTranslateAnalyzerCodeFixProvider : CodeFixProvider + { + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AnalyzerDiagnostics.OldLocalizationApiUsed.Id); + + public sealed override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + var diagnostic = context.Diagnostics.First(); + + context.RegisterCodeFix( + CodeAction.Create( + title: "Replace with 'Localize.localization_key(...args)'", + createChangedDocument: _ => Task.FromResult(FixOldTranslation(context, root, diagnostic)), + equivalenceKey: AnalyzerDiagnostics.OldLocalizationApiUsed.Id + ), + diagnostic + ); + } + + private static Document FixOldTranslation(CodeFixContext context, SyntaxNode root, Diagnostic diagnostic) + { + var diagnosticSpan = diagnostic.Location.SourceSpan; + + var invocationExpr = root + ?.FindToken(diagnosticSpan.Start).Parent + ?.AncestorsAndSelf() + .OfType() + .First(); + + if (invocationExpr is null || root is null) return context.Document; + + var argumentList = invocationExpr.ArgumentList.Arguments; + var argument = argumentList.First().Expression; + + if (GetTranslationKey(argument) is string translationKey) + return FixOldTranslationWithoutStringFormat(context, translationKey, root, invocationExpr); + + if (GetTranslationKeyFromInnerInvocation(argument) is string translationKeyInside) + return FixOldTranslationWithStringFormat(context, argumentList, translationKeyInside, root, invocationExpr); + + return context.Document; + } + + + private static string GetTranslationKey(ExpressionSyntax syntax) + { + if ( + syntax is LiteralExpressionSyntax literalExpressionSyntax && + literalExpressionSyntax.Token.Value is string translationKey + ) + return translationKey; + return null; + } + + private static Document FixOldTranslationWithoutStringFormat( + CodeFixContext context, string translationKey, SyntaxNode root, InvocationExpressionSyntax invocationExpr + ) { + var newInvocationExpr = SyntaxFactory.ParseExpression( + $"Localize.{translationKey}()" + ); + + var newRoot = root.ReplaceNode(invocationExpr, newInvocationExpr); + var newDocument = context.Document.WithSyntaxRoot(newRoot); + return newDocument; + } + + private static string GetTranslationKeyFromInnerInvocation(ExpressionSyntax syntax) + { + if ( + syntax is InvocationExpressionSyntax invocationExpressionSyntax && + invocationExpressionSyntax.ArgumentList.Arguments.Count is 1 + ) + { + var firstArgument = invocationExpressionSyntax.ArgumentList.Arguments.First().Expression; + return GetTranslationKey(firstArgument); + } + return null; + } + + private static Document FixOldTranslationWithStringFormat( + CodeFixContext context, + SeparatedSyntaxList argumentList, + string translationKey2, + SyntaxNode root, + InvocationExpressionSyntax invocationExpr + ) { + var newArguments = string.Join(", ", argumentList.Skip(1).Select(a => a.Expression)); + var newInnerInvocationExpr = SyntaxFactory.ParseExpression($"Localize.{translationKey2}({newArguments})"); + + var newRoot = root.ReplaceNode(invocationExpr, newInnerInvocationExpr); + return context.Document.WithSyntaxRoot(newRoot); + } + + } +} diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 18101ccf04e..c7c9193df82 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -13,7 +13,7 @@ false en - + true portable @@ -24,7 +24,7 @@ 4 false - + pdbonly true @@ -39,11 +39,11 @@ - + - + @@ -51,7 +51,7 @@ - + @@ -60,10 +60,22 @@ - + - \ No newline at end of file + + + + + + + diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index 96338cf6a1a..feeb978b328 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -79,14 +79,14 @@ public bool ChangeTheme(string theme) { if (string.IsNullOrEmpty(path)) throw new DirectoryNotFoundException("Theme path can't be found <{path}>"); - + // reload all resources even if the theme itself hasn't changed in order to pickup changes // to things like fonts UpdateResourceDictionary(GetResourceDictionary(theme)); - + Settings.Theme = theme; - + //always allow re-loading default theme, in case of failure of switching to a new theme from default theme if (_oldTheme != theme || theme == defaultTheme) { @@ -105,7 +105,7 @@ public bool ChangeTheme(string theme) Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found"); if (theme != defaultTheme) { - MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme)); + MessageBox.Show(Localize.theme_load_failure_path_not_exists(theme)); ChangeTheme(defaultTheme); } return false; @@ -115,7 +115,7 @@ public bool ChangeTheme(string theme) Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse"); if (theme != defaultTheme) { - MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme)); + MessageBox.Show(Localize.theme_load_failure_parse_error(theme)); ChangeTheme(defaultTheme); } return false; @@ -148,7 +148,7 @@ private ResourceDictionary GetThemeResourceDictionary(string theme) public ResourceDictionary GetResourceDictionary(string theme) { var dict = GetThemeResourceDictionary(theme); - + if (dict["QueryBoxStyle"] is Style queryBoxStyle && dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle) { @@ -189,7 +189,7 @@ public ResourceDictionary GetResourceDictionary(string theme) Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch }; Array.ForEach( - new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o + new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle, resultHotkeyItemStyle, resultHotkeyItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p))); } /* Ignore Theme Window Width and use setting */ diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 3f64b273e4c..c197347d23a 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -37,8 +37,7 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) try { if (!silentUpdate) - api.ShowMsg(api.GetTranslation("pleaseWait"), - api.GetTranslation("update_flowlauncher_update_check")); + api.ShowMsg(Localize.pleaseWait(), Localize.update_flowlauncher_update_check()); using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false); @@ -53,13 +52,12 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) if (newReleaseVersion <= currentVersion) { if (!silentUpdate) - MessageBox.Show(api.GetTranslation("update_flowlauncher_already_on_latest")); + MessageBox.Show(Localize.update_flowlauncher_already_on_latest()); return; } if (!silentUpdate) - api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"), - api.GetTranslation("update_flowlauncher_updating")); + api.ShowMsg(Localize.update_flowlauncher_update_found(), Localize.update_flowlauncher_updating()); await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false); @@ -70,20 +68,22 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}"; FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination); if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination)) - MessageBox.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), - DataLocation.PortableDataPath, - targetDestination)); + MessageBox.Show( + Localize.update_flowlauncher_fail_moving_portable_user_profile_data( + DataLocation.PortableDataPath, targetDestination + ) + ); } else { await updateManager.CreateUninstallerRegistryEntry().ConfigureAwait(false); } - var newVersionTips = NewVersionTips(newReleaseVersion.ToString()); + var newVersionTips = Localize.newVersionTips(newReleaseVersion.ToString()); Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}"); - if (MessageBox.Show(newVersionTips, api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes) + if (MessageBox.Show(newVersionTips, Localize.update_flowlauncher_new_update(), MessageBoxButton.YesNo) == MessageBoxResult.Yes) { UpdateManager.RestartApp(Constant.ApplicationFileName); } @@ -94,10 +94,9 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); else Log.Exception($"|Updater.UpdateApp|Error Occurred", e); - + if (!silentUpdate) - api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"), - api.GetTranslation("update_flowlauncher_check_connection")); + api.ShowMsg(Localize.update_flowlauncher_fail(), Localize.update_flowlauncher_check_connection()); } finally { @@ -140,13 +139,5 @@ private async Task GitHubUpdateManagerAsync(string repository) return manager; } - - public string NewVersionTips(string version) - { - var translator = InternationalizationManager.Instance; - var tips = string.Format(translator.GetTranslation("newVersionTips"), version); - - return tips; - } } } diff --git a/Flow.Launcher.SourceGenerators/AnalyzerReleases.Shipped.md b/Flow.Launcher.SourceGenerators/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000000..5ccc9f037f6 --- /dev/null +++ b/Flow.Launcher.SourceGenerators/AnalyzerReleases.Shipped.md @@ -0,0 +1,2 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/Flow.Launcher.SourceGenerators/AnalyzerReleases.Unshipped.md b/Flow.Launcher.SourceGenerators/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000000..f60d25850da --- /dev/null +++ b/Flow.Launcher.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,13 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +FLSG0001 | Localization | Warning | FLSG0001_CouldNotFindResourceDictionaries +FLSG0002 | Localization | Warning | FLSG0002_CouldNotFindPluginEntryClass +FLSG0003 | Localization | Warning | FLSG0003_CouldNotFindContextProperty +FLSG0004 | Localization | Warning | FLSG0004_ContextPropertyNotStatic +FLSG0005 | Localization | Warning | FLSG0005_ContextPropertyIsPrivate +FLSG0006 | Localization | Warning | FLSG0006_ContextPropertyIsProtected +FLSG0007 | Localization | Warning | FLSG0007_LocalizationKeyUnused diff --git a/Flow.Launcher.SourceGenerators/Flow.Launcher.SourceGenerators.csproj b/Flow.Launcher.SourceGenerators/Flow.Launcher.SourceGenerators.csproj new file mode 100644 index 00000000000..9679baa17e0 --- /dev/null +++ b/Flow.Launcher.SourceGenerators/Flow.Launcher.SourceGenerators.csproj @@ -0,0 +1,17 @@ + + + + 1.0.0 + netstandard2.0 + true + Flow.Launcher.SourceGenerators + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Flow.Launcher.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.SourceGenerators/Localize/LocalizeSourceGenerator.cs new file mode 100644 index 00000000000..ca8d6d5a01d --- /dev/null +++ b/Flow.Launcher.SourceGenerators/Localize/LocalizeSourceGenerator.cs @@ -0,0 +1,491 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Flow.Launcher.SourceGenerators.Localize +{ + [Generator] + public partial class LocalizeSourceGenerator : ISourceGenerator + { + private OptimizationLevel _optimizationLevel; + + private const string CoreNamespace1 = "Flow.Launcher"; + private const string CoreNamespace2 = "Flow.Launcher.Core"; + private const string DefaultNamespace = "Flow.Launcher"; + private const string ClassName = "Localize"; + private const string PluginInterfaceName = "IPluginI18n"; + private const string PluginContextTypeName = "PluginInitContext"; + private const string KeywordStatic = "static"; + private const string KeywordPrivate = "private"; + private const string KeywordProtected = "protected"; + private const string XamlPrefix = "system"; + private const string XamlTag = "String"; + + private const string DefaultLanguageFilePathEndsWith = @"\Languages\en.xaml"; + private const string XamlCustomPathPropertyKey = "build_property.localizegeneratorlangfiles"; + private readonly char[] _xamlCustomPathPropertyDelimiters = { '\n', ';' }; + private readonly Regex _languagesXamlRegex = new Regex(@"\\Languages\\[^\\]+\.xaml$", RegexOptions.IgnoreCase); + + public void Initialize(GeneratorInitializationContext context) + { + } + + public void Execute(GeneratorExecutionContext context) + { + _optimizationLevel = context.Compilation.Options.OptimizationLevel; + + context.AnalyzerConfigOptions.GlobalOptions.TryGetValue( + XamlCustomPathPropertyKey, + out var langFilePathEndsWithStr + ); + + var allLanguageKeys = new List(); + context.Compilation.SyntaxTrees + .SelectMany(v => v.GetRoot().DescendantNodes().OfType()) + .ToList() + .ForEach( + v => + { + var split = v.Expression.ToString().Split('.'); + if (split.Length < 2) return; + if (!(split[0] is ClassName)) return; + allLanguageKeys.Add(split[1]); + }); + + var allXamlFiles = context.AdditionalFiles + .Where(v => _languagesXamlRegex.IsMatch(v.Path)) + .ToArray(); + AdditionalText[] resourceDictionaries; + if (allXamlFiles.Length is 0) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries, + Location.None + )); + return; + } + + if (string.IsNullOrEmpty(langFilePathEndsWithStr)) + { + if (allXamlFiles.Length is 1) + { + resourceDictionaries = allXamlFiles; + } + else + { + resourceDictionaries = allXamlFiles.Where(v => v.Path.EndsWith(DefaultLanguageFilePathEndsWith)).ToArray(); + if (resourceDictionaries.Length is 0) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries, + Location.None + )); + return; + } + } + } + else + { + var langFilePathEndings = langFilePathEndsWithStr + .Trim() + .Split(_xamlCustomPathPropertyDelimiters) + .Select(v => v.Trim()) + .ToArray(); + resourceDictionaries = allXamlFiles.Where(v => langFilePathEndings.Any(v.Path.EndsWith)).ToArray(); + if (resourceDictionaries.Length is 0) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries, + Location.None + )); + return; + } + } + + var ns = context.Compilation.AssemblyName ?? DefaultNamespace; + + var localizedStrings = LoadLocalizedStrings(resourceDictionaries); + + var unusedLocalizationKeys = localizedStrings.Keys.Except(allLanguageKeys).ToArray(); + + foreach (var key in unusedLocalizationKeys) + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.LocalizationKeyUnused, + Location.None, + key + )); + + var sourceCode = GenerateSourceCode(localizedStrings, context, unusedLocalizationKeys); + + context.AddSource($"{ClassName}.{ns}.g.cs", SourceText.From(sourceCode, Encoding.UTF8)); + } + + private static Dictionary LoadLocalizedStrings(AdditionalText[] files) + { + var result = new Dictionary(); + + foreach (var file in files) + { + ProcessXamlFile(file, result); + } + + return result; + } + + private static void ProcessXamlFile(AdditionalText file, Dictionary result) { + var content = file.GetText()?.ToString(); + if (content is null) return; + var doc = XDocument.Parse(content); + var ns = doc.Root?.GetNamespaceOfPrefix(XamlPrefix); + if (ns is null) return; + foreach (var element in doc.Descendants(ns + XamlTag)) + { + var name = element.FirstAttribute?.Value; + var value = element.Value; + + if (name is null) continue; + + string summary = null; + var paramsList = new List(); + var commentNode = element.PreviousNode; + + if (commentNode is XComment comment) + summary = ProcessXamlFileComment(comment, paramsList); + + result[name] = new LocalizableString(name, value, summary, paramsList); + } + } + + private static string ProcessXamlFileComment(XComment comment, List paramsList) { + string summary = null; + try + { + if (CommentIncludesDocumentationMarkup(comment)) + { + var commentDoc = XDocument.Parse($"{comment.Value}"); + summary = ExtractDocumentationCommentSummary(commentDoc); + foreach (var param in commentDoc.Descendants("param")) + { + var index = int.Parse(param.Attribute("index")?.Value ?? "-1"); + var paramName = param.Attribute("name")?.Value; + var paramType = param.Attribute("type")?.Value; + if (index < 0 || paramName is null || paramType is null) continue; + paramsList.Add(new LocalizableStringParam(index, paramName, paramType)); + } + } + } + catch + { + // ignore + } + + return summary; + } + + private static string ExtractDocumentationCommentSummary(XDocument commentDoc) { + return commentDoc.Descendants("summary").FirstOrDefault()?.Value.Trim(); + } + + private static bool CommentIncludesDocumentationMarkup(XComment comment) { + return comment.Value.Contains("") || comment.Value.Contains(" localizedStrings, + GeneratorExecutionContext context, + string[] unusedLocalizationKeys + ) + { + var ns = context.Compilation.AssemblyName; + + var sb = new StringBuilder(); + if (ns is CoreNamespace1 || ns is CoreNamespace2) + { + GenerateFileHeader(sb, context); + GenerateClass(sb, localizedStrings, unusedLocalizationKeys); + return sb.ToString(); + } + + string contextPropertyName = null; + var mainClassFound = false; + foreach (var (syntaxTree, classDeclaration) in GetClasses(context)) + { + if (!DoesClassImplementInterface(classDeclaration, PluginInterfaceName)) + continue; + + mainClassFound = true; + + var property = GetPluginContextProperty(classDeclaration); + if (property is null) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.CouldNotFindContextProperty, + GetLocation(syntaxTree, classDeclaration), + classDeclaration.Identifier + )); + return string.Empty; + } + + var propertyModifiers = GetPropertyModifiers(property); + + if (!propertyModifiers.Static) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.ContextPropertyNotStatic, + GetLocation(syntaxTree, property), + property.Identifier + )); + return string.Empty; + } + + if (propertyModifiers.Private) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.ContextPropertyIsPrivate, + GetLocation(syntaxTree, property), + property.Identifier + )); + return string.Empty; + } + + if (propertyModifiers.Protected) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.ContextPropertyIsProtected, + GetLocation(syntaxTree, property), + property.Identifier + )); + return string.Empty; + } + + contextPropertyName = $"{classDeclaration.Identifier}.{property.Identifier}"; + break; + } + + if (mainClassFound is false) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.CouldNotFindPluginEntryClass, + Location.None + )); + return string.Empty; + } + + GenerateFileHeader(sb, context, true); + GenerateClass(sb, localizedStrings, unusedLocalizationKeys, contextPropertyName); + return sb.ToString(); + } + + private static void GenerateFileHeader(StringBuilder sb, GeneratorExecutionContext context, bool isPlugin = false) + { + var rootNamespace = context.Compilation.AssemblyName; + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + + if (!isPlugin) + sb.AppendLine("using Flow.Launcher.Core.Resource;"); + + sb.AppendLine($"namespace {rootNamespace};"); + } + + private void GenerateClass( + StringBuilder sb, + Dictionary localizedStrings, + string[] unusedLocalizationKeys, + string propertyName = null + ) + { + sb.AppendLine(); + sb.AppendLine($"public static class {ClassName}"); + sb.AppendLine("{"); + foreach (var localizedString in localizedStrings) + { + if (_optimizationLevel == OptimizationLevel.Release && unusedLocalizationKeys.Contains(localizedString.Key)) + continue; + + GenerateDocCommentForMethod(sb, localizedString.Value); + GenerateMethod(sb, localizedString.Value, propertyName); + } + + sb.AppendLine("}"); + } + + private static void GenerateDocCommentForMethod(StringBuilder sb, LocalizableString localizableString) + { + sb.AppendLine("/// "); + if (!(localizableString.Summary is null)) + { + sb.AppendLine(string.Join("\n", localizableString.Summary.Trim().Split('\n').Select(v => $"/// {v}"))); + } + + sb.AppendLine("/// "); + var value = localizableString.Value; + foreach (var p in localizableString.Params) + { + value = value.Replace($"{{{p.Index}}}", $"{{{p.Name}}}"); + } + sb.AppendLine(string.Join("\n", value.Split('\n').Select(v => $"/// {v}"))); + sb.AppendLine("/// "); + sb.AppendLine("/// "); + } + + private static void GenerateMethod(StringBuilder sb, LocalizableString localizableString, string contextPropertyName) + { + sb.Append($"public static string {localizableString.Key}("); + var declarationArgs = new List(); + var callArgs = new List(); + for (var i = 0; i < 10; i++) + { + if (localizableString.Value.Contains($"{{{i}}}")) + { + var param = localizableString.Params.FirstOrDefault(v => v.Index == i); + if (!(param is null)) + { + declarationArgs.Add($"{param.Type} {param.Name}"); + callArgs.Add(param.Name); + } + else + { + declarationArgs.Add($"object? arg{i}"); + callArgs.Add($"arg{i}"); + } + } + else + { + break; + } + } + + string callArray; + switch (callArgs.Count) + { + case 0: + callArray = ""; + break; + case 1: + callArray = callArgs[0]; + break; + default: + callArray = $"new object?[] {{ {string.Join(", ", callArgs)} }}"; + break; + } + + sb.Append(string.Join(", ", declarationArgs)); + sb.Append(") => "); + if (contextPropertyName is null) + { + if (string.IsNullOrEmpty(callArray)) + { + sb.AppendLine($"InternationalizationManager.Instance.GetTranslation(\"{localizableString.Key}\");"); + } + else + { + sb.AppendLine( + $"string.Format(InternationalizationManager.Instance.GetTranslation(\"{localizableString.Key}\"), {callArray});" + ); + } + } + else + { + if (string.IsNullOrEmpty(callArray)) + { + sb.AppendLine($"{contextPropertyName}.API.GetTranslation(\"{localizableString.Key}\");"); + } + else + { + sb.AppendLine($"string.Format({contextPropertyName}.API.GetTranslation(\"{localizableString.Key}\"), {callArray});"); + } + } + + sb.AppendLine(); + } + + private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode classDeclaration) + { + return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan); + } + + private static IEnumerable<(SyntaxTree, ClassDeclarationSyntax)> GetClasses(GeneratorExecutionContext context) + { + foreach (var syntaxTree in context.Compilation.SyntaxTrees) + { + var classDeclarations = syntaxTree.GetRoot().DescendantNodes().OfType(); + foreach (var classDeclaration in classDeclarations) + { + yield return (syntaxTree, classDeclaration); + } + } + } + + private static bool DoesClassImplementInterface(ClassDeclarationSyntax classDeclaration, string interfaceName) + { + return classDeclaration.BaseList?.Types.Any(v => interfaceName == v.ToString()) is true; + } + + private static PropertyDeclarationSyntax GetPluginContextProperty(ClassDeclarationSyntax classDeclaration) + { + return classDeclaration.Members + .OfType() + .FirstOrDefault(v => v.Type.ToString() is PluginContextTypeName); + } + + private static Modifiers GetPropertyModifiers(PropertyDeclarationSyntax property) + { + var isStatic = property.Modifiers.Any(v => v.Text is KeywordStatic); + var isPrivate = property.Modifiers.Any(v => v.Text is KeywordPrivate); + var isProtected = property.Modifiers.Any(v => v.Text is KeywordProtected); + + return new Modifiers(isStatic, isPrivate, isProtected); + } + + private class Modifiers + { + public bool Static { get; } + public bool Private { get; } + public bool Protected { get; } + + public Modifiers(bool isStatic = false, bool isPrivate = false, bool isProtected = false) + { + Static = isStatic; + Private = isPrivate; + Protected = isProtected; + } + } + } + + public class LocalizableStringParam + { + public int Index { get; } + public string Name { get; } + public string Type { get; } + + public LocalizableStringParam(int index, string name, string type) + { + Index = index; + Name = name; + Type = type; + } + } + + public class LocalizableString + { + public string Key { get; } + public string Value { get; } + public string Summary { get; } + public IEnumerable Params { get; } + + public LocalizableString(string key, string value, string summary, IEnumerable @params) + { + Key = key; + Value = value; + Summary = summary; + Params = @params; + } + } +} diff --git a/Flow.Launcher.SourceGenerators/SourceGeneratorDiagnostics.cs b/Flow.Launcher.SourceGenerators/SourceGeneratorDiagnostics.cs new file mode 100644 index 00000000000..296419bd3e1 --- /dev/null +++ b/Flow.Launcher.SourceGenerators/SourceGeneratorDiagnostics.cs @@ -0,0 +1,70 @@ +using Microsoft.CodeAnalysis; + +namespace Flow.Launcher.SourceGenerators +{ + public static class SourceGeneratorDiagnostics + { + public static readonly DiagnosticDescriptor CouldNotFindResourceDictionaries = new DiagnosticDescriptor( + "FLSG0001", + "Could not find resource dictionaries", + "Could not find resource dictionaries. There must be a file named [LANG].xaml file (for example, en.xaml), and it must be specified in in your .csproj file.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor CouldNotFindPluginEntryClass = new DiagnosticDescriptor( + "FLSG0002", + "Could not find the main class of plugin", + "Could not find the main class of your plugin. It must implement IPluginI18n.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor CouldNotFindContextProperty = new DiagnosticDescriptor( + "FLSG0003", + "Could not find plugin context property", + "Could not find a property of type PluginInitContext in {0}. It must be a public static or internal static property of the main class of your plugin.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ContextPropertyNotStatic = new DiagnosticDescriptor( + "FLSG0004", + "Plugin context property is not static", + "Context property {0} is not static. It must be static.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ContextPropertyIsPrivate = new DiagnosticDescriptor( + "FLSG0005", + "Plugin context property is private", + "Context property {0} is private. It must be either internal or public.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ContextPropertyIsProtected = new DiagnosticDescriptor( + "FLSG0006", + "Plugin context property is protected", + "Context property {0} is protected. It must be either internal or public.", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor LocalizationKeyUnused = new DiagnosticDescriptor( + "FLSG0007", + "Localization key is unused", + "Method 'Localize.{0}' is never used", + "Localization", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + } +} diff --git a/Flow.Launcher.sln b/Flow.Launcher.sln index e44b23232fb..bdbcdc4d7db 100644 --- a/Flow.Launcher.sln +++ b/Flow.Launcher.sln @@ -71,6 +71,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Plugin EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.WindowsSettings", "Plugins\Flow.Launcher.Plugin.WindowsSettings\Flow.Launcher.Plugin.WindowsSettings.csproj", "{5043CECE-E6A7-4867-9CBE-02D27D83747A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Launcher.SourceGenerators", "Flow.Launcher.SourceGenerators\Flow.Launcher.SourceGenerators.csproj", "{2CB3B152-6147-48E2-B497-CEFA3331E9B4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Launcher.Analyzers", "Flow.Launcher.Analyzers\Flow.Launcher.Analyzers.csproj", "{58A54F2B-772F-4724-861D-F3A191441DF1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -82,7 +86,7 @@ Global EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.ActiveCfg = Debug|Any CPU {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.Build.0 = Debug|Any CPU {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -286,6 +290,30 @@ Global {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x64.Build.0 = Release|Any CPU {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x86.ActiveCfg = Release|Any CPU {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x86.Build.0 = Release|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|x64.ActiveCfg = Debug|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|x64.Build.0 = Debug|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|x86.ActiveCfg = Debug|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Debug|x86.Build.0 = Debug|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|Any CPU.Build.0 = Release|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|x64.ActiveCfg = Release|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|x64.Build.0 = Release|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|x86.ActiveCfg = Release|Any CPU + {2CB3B152-6147-48E2-B497-CEFA3331E9B4}.Release|x86.Build.0 = Release|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|x64.ActiveCfg = Debug|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|x64.Build.0 = Debug|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|x86.ActiveCfg = Debug|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Debug|x86.Build.0 = Debug|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|Any CPU.Build.0 = Release|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|x64.ActiveCfg = Release|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|x64.Build.0 = Release|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|x86.ActiveCfg = Release|Any CPU + {58A54F2B-772F-4724-861D-F3A191441DF1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 070c290cdfb..cab0da722c1 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -146,7 +146,9 @@ Result Item Font Window Mode Opacity + Theme {0} not exists, fallback to default theme + Fail to load theme {0}, fallback to default theme Theme Folder Open Theme Folder @@ -254,6 +256,7 @@ You have activated Flow Launcher {0} times Check for Updates Become A Sponsor + New version {0} is available, would you like to restart Flow Launcher to use the update? Check updates failed, please check your connection and proxy settings to api.github.com. @@ -358,6 +361,8 @@ You already have the latest Flow Launcher version Update found Updating... + Flow Launcher was not able to move your user profile data to the new update version. Please manually move your profile data folder from {0} to {1} diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj index fe118c2c3bf..0ce9d6d458f 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj @@ -59,4 +59,16 @@ - \ No newline at end of file + + + + + + + diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj index 415f852f4c8..81170d95f85 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj @@ -13,7 +13,7 @@ false en - + true portable @@ -24,7 +24,7 @@ 4 false - + pdbonly true @@ -34,13 +34,13 @@ 4 false - + PreserveNewest - + @@ -64,5 +64,17 @@ - - \ No newline at end of file + + + + + + + +