Skip to content

Commit e921fd0

Browse files
authored
Code Quality: Generative resource management and refactoring (#15662)
1 parent 94b337e commit e921fd0

20 files changed

+836
-99
lines changed

Files.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,4 +626,4 @@ Global
626626
GlobalSection(ExtensibilityGlobals) = postSolution
627627
SolutionGuid = {0E62043C-A7A1-4982-9EC9-4CDB2939B776}
628628
EndGlobalSection
629-
EndGlobal
629+
EndGlobal

src/Files.App/Files.App.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,5 +133,9 @@
133133
<TrimmerRootAssembly Include="CommunityToolkit.WinUI.UI.Controls.Media" />
134134
<TrimmerRootAssembly Include="CommunityToolkit.WinUI.UI.Controls.Primitives" />
135135
</ItemGroup>
136+
137+
<ItemGroup>
138+
<AdditionalFiles Include="Strings\en-US\Resources.resw" />
139+
</ItemGroup>
136140

137141
</Project>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace Files.App.Helpers
8+
{
9+
public sealed partial class Strings
10+
{
11+
}
12+
}

src/Files.App/ViewModels/Properties/Items/FileProperty.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ private IValueConverter GetConverter()
231231
{
232232
try
233233
{
234-
return EnumeratedList.TryGetValue(Convert.ToInt32(Value), out var value) ?
234+
return EnumeratedList.TryGetValue(Convert.ToInt32(Value), out var value) ?
235235
value.GetLocalizedResource() : null;
236236
}
237237
catch

src/Files.Core.SourceGenerator/AnalyzerReleases.Unshipped.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@
77
Rule ID | Category | Severity | Notes
88
--------|----------|----------|--------------------
99
FSG1001 | Design | Error | FSG1001_Files.Core.SourceGenerator
10+
FSG1002 | Refactoring | Warning | FSG1002_Files.Core.SourceGenerator
11+
FSG1003 | FileGeneration | Error | FSG1003_Files.Core.SourceGenerator
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) 2024 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using static Files.Core.SourceGenerator.Constants.DiagnosticDescriptors;
5+
using static Files.Core.SourceGenerator.Constants.StringsPropertyGenerator;
6+
7+
namespace Files.Core.SourceGenerator.Analyzers
8+
{
9+
/// <summary>
10+
/// Analyzer that detects if string literals can be replaced with constants from the <c>Strings</c> class.
11+
/// </summary>
12+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
13+
internal sealed class StringsPropertyAnalyzer : DiagnosticAnalyzer
14+
{
15+
/// <summary>
16+
/// Represents a collection of constant string names and their corresponding values.
17+
/// </summary>
18+
internal static FrozenDictionary<string, string?>? StringsConstants { get; private set; } = null;
19+
20+
/// <summary>
21+
/// Gets the supported diagnostics for this analyzer.
22+
/// </summary>
23+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [FSG1002];
24+
25+
/// <summary>
26+
/// Initializes the analyzer and registers the syntax node action.
27+
/// </summary>
28+
/// <param name="context">The analysis context.</param>
29+
public override void Initialize(AnalysisContext context)
30+
{
31+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
32+
context.EnableConcurrentExecution();
33+
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.StringLiteralExpression, SyntaxKind.InterpolatedStringText);
34+
}
35+
36+
/// <summary>
37+
/// Analyzes the syntax node to detect if a string literal can be replaced with a constant from the Strings class.
38+
/// </summary>
39+
/// <param name="context">The syntax node analysis context.</param>
40+
private void AnalyzeNode(SyntaxNodeAnalysisContext context)
41+
{
42+
var literalValue = string.Empty;
43+
Location locationValue = default!;
44+
SyntaxNode? parent = null;
45+
46+
if (context.Node is InterpolatedStringTextSyntax interpolatedStringText)
47+
{
48+
literalValue = interpolatedStringText.TextToken.ValueText;
49+
locationValue = interpolatedStringText.GetLocation();
50+
parent = interpolatedStringText.Parent?.Parent;
51+
}
52+
53+
if (context.Node is LiteralExpressionSyntax literalExpression)
54+
{
55+
literalValue = literalExpression.Token.ValueText;
56+
locationValue = literalExpression.GetLocation();
57+
parent = literalExpression.Parent;
58+
}
59+
60+
if (string.IsNullOrEmpty(literalValue) ||
61+
parent is not MemberAccessExpressionSyntax memberAccessExpression ||
62+
!LocalizedMethodNames.Contains(memberAccessExpression.Name.Identifier.Text))
63+
return;
64+
65+
if (StringsConstants is null)
66+
{
67+
var semanticModel = context.SemanticModel;
68+
var compilation = semanticModel.Compilation;
69+
70+
// Get the type symbol for the Strings class
71+
var stringsTypeSymbol = compilation.GetTypeByMetadataName(StringsMetadataName);
72+
73+
if (stringsTypeSymbol == null)
74+
return;
75+
76+
// Extract constants from the Strings class
77+
StringsConstants = stringsTypeSymbol.GetMembers()
78+
.OfType<IFieldSymbol>()
79+
.Where(f => f.IsConst)
80+
.ToDictionary(f => f.Name, f => f.ConstantValue?.ToString())
81+
.ToFrozenDictionary();
82+
}
83+
84+
// Check if the literal value matches any of the constants
85+
var match = StringsConstants.FirstOrDefault(pair => pair.Value == literalValue);
86+
87+
if (!match.Equals(default(KeyValuePair<string, string>)))
88+
{
89+
var properties = ImmutableDictionary<string, string>
90+
.Empty
91+
.Add(ConstantNameProperty, match.Key);
92+
93+
var diagnostic = Diagnostic.Create(FSG1002, locationValue, properties!, literalValue, match.Key);
94+
context.ReportDiagnostic(diagnostic);
95+
}
96+
}
97+
}
98+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) 2024 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using static Files.Core.SourceGenerator.Constants.DiagnosticDescriptors;
5+
using static Files.Core.SourceGenerator.Constants.StringsPropertyGenerator;
6+
7+
namespace Files.Core.SourceGenerator.CodeFixProviders
8+
{
9+
/// <summary>
10+
/// Code fix provider that replaces string literals with constants from the <c>Strings</c> class.
11+
/// </summary>
12+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(StringsPropertyCodeFixProvider)), Shared]
13+
internal sealed class StringsPropertyCodeFixProvider : CodeFixProvider
14+
{
15+
/// <summary>
16+
/// Gets a list of diagnostic IDs that this provider can fix.
17+
/// </summary>
18+
public sealed override ImmutableArray<string> FixableDiagnosticIds => [FSG1002.Id];
19+
20+
/// <summary>
21+
/// Gets the fix all provider for this code fix provider.
22+
/// </summary>
23+
/// <returns>A <see cref="FixAllProvider"/>.</returns>
24+
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
25+
26+
/// <summary>
27+
/// Registers code fixes for the specified diagnostic.
28+
/// </summary>
29+
/// <param name="context">The context for the code fix.</param>
30+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
31+
{
32+
var diagnostic = context.Diagnostics.First();
33+
34+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
35+
if (root == null)
36+
return;
37+
38+
var diagnosticSpan = diagnostic.Location.SourceSpan;
39+
40+
SyntaxNode? node = null;
41+
42+
if (root.FindNode(diagnosticSpan) is LiteralExpressionSyntax literalExpression)
43+
node = literalExpression;
44+
45+
if (root.FindNode(diagnosticSpan) is InterpolatedStringTextSyntax interpolatedStringText)
46+
node = interpolatedStringText.Parent;
47+
48+
if (node is null)
49+
return;
50+
51+
var constantName = diagnostic.Properties[ConstantNameProperty];
52+
var newExpression = SyntaxFactory.ParseExpression($"{StringsClassName}.{constantName}").WithTriviaFrom(node);
53+
var newRoot = root.ReplaceNode(node, newExpression);
54+
var newDocument = context.Document.WithSyntaxRoot(newRoot);
55+
56+
context.RegisterCodeFix(
57+
CodeAction.Create(
58+
CodeFixProviderTitle,
59+
c => Task.FromResult(newDocument),
60+
null),
61+
diagnostic);
62+
}
63+
}
64+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) 2024 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
namespace Files.Core.SourceGenerator
5+
{
6+
/// <summary>
7+
/// Contains various constants used within the source generator.
8+
/// </summary>
9+
internal class Constants
10+
{
11+
/// <summary>
12+
/// Contains diagnostic descriptors used for error reporting.
13+
/// </summary>
14+
internal class DiagnosticDescriptors
15+
{
16+
/// <summary>
17+
/// Diagnostic descriptor for unsupported types in Windows Registry.
18+
/// </summary>
19+
internal static readonly DiagnosticDescriptor FSG1001 = new(
20+
id: nameof(FSG1001),
21+
title: "Types that are not supported by Windows Registry",
22+
messageFormat: "Type '{0}' is not supported by Windows Registry",
23+
category: "Design",
24+
defaultSeverity: DiagnosticSeverity.Error,
25+
isEnabledByDefault: true);
26+
27+
/// <summary>
28+
/// Diagnostic descriptor for a refactoring suggestion to replace string literals with constants from the Strings class.
29+
/// </summary>
30+
internal static readonly DiagnosticDescriptor FSG1002 = new(
31+
id: nameof(FSG1002),
32+
title: "String literal can be replaced with constant",
33+
messageFormat: $"Replace '{{0}}' with '{StringsPropertyGenerator.StringsClassName}.{{1}}'",
34+
category: "Refactoring",
35+
defaultSeverity: DiagnosticSeverity.Warning,
36+
isEnabledByDefault: true,
37+
description: $"Detects string literals that can be replaced with constants from the {StringsPropertyGenerator.StringsClassName} class.");
38+
39+
/// <summary>
40+
/// Diagnostic descriptor for a scenario where multiple files with the same name are detected.
41+
/// </summary>
42+
internal static readonly DiagnosticDescriptor FSG1003 = new(
43+
id: nameof(FSG1003),
44+
title: "Multiple files with the same name detected",
45+
messageFormat: "Multiple files named '{0}' were detected. Ensure all generated localization string files have unique names.",
46+
category: "FileGeneration",
47+
defaultSeverity: DiagnosticSeverity.Error,
48+
isEnabledByDefault: true,
49+
description: "This diagnostic detects cases where multiple localization string files are being generated with the same name," +
50+
"which can cause conflicts and overwrite issues.");
51+
52+
}
53+
54+
/// <summary>
55+
/// Contains constants related to DependencyProperty generation.
56+
/// </summary>
57+
internal class DependencyPropertyGenerator
58+
{
59+
/// <summary>
60+
/// The name of the attribute used for DependencyProperty.
61+
/// </summary>
62+
internal static readonly string AttributeName = "DependencyPropertyAttribute";
63+
}
64+
65+
internal class StringsPropertyGenerator
66+
{
67+
/// <summary>
68+
/// The name of the generated class that contains string constants.
69+
/// </summary>
70+
internal const string StringsClassName = "Strings";
71+
72+
/// <summary>
73+
/// The fully qualified name of the generated metadata class that contains string constants.
74+
/// </summary>
75+
internal const string StringsMetadataName = $"{SourceGeneratorHelper.HelperNamespace}{StringsClassName}";
76+
77+
/// <summary>
78+
/// The name of the property that represents the name of the constant.
79+
/// </summary>
80+
internal const string ConstantNameProperty = nameof(ConstantNameProperty);
81+
82+
/// <summary>
83+
/// A collection of method names that are considered localized methods.
84+
/// These methods are used to identify string literals that can be replaced with constants from the Strings class.
85+
/// </summary>
86+
internal static HashSet<string> LocalizedMethodNames = [
87+
/* TODO: Future use only this */ "ToLocalized",
88+
/* TODO: Rewrite with ToLocalized */ "GetLocalizedResource",
89+
/* TODO: Rewrite with ToLocalized */ "GetLocalizedFormatResource"];
90+
91+
/// <summary>
92+
/// The title of the code fix provider that suggests replacing string literals with constants from the Strings class.
93+
/// </summary>
94+
internal const string CodeFixProviderTitle = $"Replace with constant from {StringsClassName}";
95+
96+
/// <summary>
97+
/// Represents a character used as a separator in constant names.
98+
/// </summary>
99+
internal const char ConstantSeparator = '/';
100+
}
101+
}
102+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) 2024 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
namespace Files.Core.SourceGenerator.Data
5+
{
6+
/// <summary>
7+
/// Represents an item used for parsing data.
8+
/// </summary>
9+
internal class ParserItem
10+
{
11+
/// <summary>
12+
/// Gets or sets the key of the item.
13+
/// </summary>
14+
/// <remarks>
15+
/// This property is required and cannot be null or empty.
16+
/// </remarks>
17+
internal required string Key { get; set; } = default!;
18+
19+
/// <summary>
20+
/// Gets or sets the value of the item.
21+
/// </summary>
22+
/// <remarks>
23+
/// The default value is an empty string.
24+
/// </remarks>
25+
internal string Value { get; set; } = string.Empty;
26+
27+
/// <summary>
28+
/// Gets or sets the comment associated with the item.
29+
/// </summary>
30+
/// <remarks>
31+
/// The default value is null, indicating no comment.
32+
/// </remarks>
33+
internal string? Comment { get; set; } = null;
34+
}
35+
}

src/Files.Core.SourceGenerator/DiagnosticDescriptors.cs

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)