Skip to content

Commit b858737

Browse files
Update SA1649CodeFixProvider to use method WithDocumentName in Solution if it is available #2277
1 parent 834380f commit b858737

File tree

3 files changed

+129
-8
lines changed

3 files changed

+129
-8
lines changed

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1649CodeFixProvider.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4-
#nullable disable
5-
64
namespace StyleCop.Analyzers.DocumentationRules
75
{
86
using System.Collections.Immutable;
@@ -14,6 +12,7 @@ namespace StyleCop.Analyzers.DocumentationRules
1412
using Microsoft.CodeAnalysis.CodeActions;
1513
using Microsoft.CodeAnalysis.CodeFixes;
1614
using StyleCop.Analyzers.Helpers;
15+
using StyleCop.Analyzers.Lightup;
1716

1817
/// <summary>
1918
/// Implements a code fix for <see cref="SA1649FileNameMustMatchTypeName"/>.
@@ -27,7 +26,7 @@ internal class SA1649CodeFixProvider : CodeFixProvider
2726
ImmutableArray.Create(SA1649FileNameMustMatchTypeName.DiagnosticId);
2827

2928
/// <inheritdoc/>
30-
public override FixAllProvider GetFixAllProvider()
29+
public override FixAllProvider? GetFixAllProvider()
3130
{
3231
// The batch fixer can't handle code fixes that create new files
3332
return null;
@@ -55,25 +54,32 @@ private static async Task<Solution> GetTransformedSolutionAsync(Document documen
5554
var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
5655
var expectedFileName = diagnostic.Properties[SA1649FileNameMustMatchTypeName.ExpectedFileNameKey];
5756

58-
var newSolution = ReplaceDocument(solution, document, document.Id, syntaxRoot, expectedFileName);
57+
var newSolution = RenameDocument(solution, document, document.Id, syntaxRoot, expectedFileName);
5958

6059
// Make sure to also update other projects which reference the same file
6160
foreach (var linkedDocumentId in document.GetLinkedDocumentIds())
6261
{
63-
newSolution = ReplaceDocument(newSolution, null, linkedDocumentId, syntaxRoot, expectedFileName);
62+
newSolution = RenameDocument(newSolution, null, linkedDocumentId, syntaxRoot, expectedFileName);
6463
}
6564

6665
return newSolution;
6766
}
6867

69-
private static Solution ReplaceDocument(Solution solution, Document document, DocumentId documentId, SyntaxNode syntaxRoot, string expectedFileName)
68+
private static Solution RenameDocument(Solution solution, Document? document, DocumentId documentId, SyntaxNode syntaxRoot, string expectedFileName)
7069
{
71-
document ??= solution.GetDocument(documentId);
70+
// First try to use the "new" WithDocumentName method. This will return null if it is not available in the current Roslyn version.
71+
var newSolution = solution.WithDocumentName(documentId, expectedFileName);
72+
if (newSolution != null)
73+
{
74+
return newSolution;
75+
}
7276

77+
// Continue by instead removing and re-adding the file again
78+
document ??= solution.GetDocument(documentId);
7379
var newDocumentFilePath = document.FilePath != null ? Path.Combine(Path.GetDirectoryName(document.FilePath), expectedFileName) : null;
7480
var newDocumentId = DocumentId.CreateNewId(documentId.ProjectId);
7581

76-
var newSolution = solution
82+
newSolution = solution
7783
.RemoveDocument(documentId)
7884
.AddDocument(newDocumentId, expectedFileName, syntaxRoot, document.Folders, newDocumentFilePath);
7985
return newSolution;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
namespace StyleCop.Analyzers.Lightup
5+
{
6+
using System;
7+
using Microsoft.CodeAnalysis;
8+
9+
internal static class SolutionExtensions
10+
{
11+
private static readonly Func<Solution, DocumentId, string, Solution> WithDocumentNameAccessor;
12+
13+
static SolutionExtensions()
14+
{
15+
WithDocumentNameAccessor = LightupHelpers.CreateSyntaxPropertyAccessor<Solution, DocumentId, string, Solution>(typeof(Solution), typeof(DocumentId), typeof(string), nameof(WithDocumentName));
16+
}
17+
18+
public static Solution WithDocumentName(this Solution solution, DocumentId documentId, string name)
19+
{
20+
return WithDocumentNameAccessor(solution, documentId, name);
21+
}
22+
}
23+
}

StyleCop.Analyzers/StyleCop.Analyzers/Lightup/LightupHelpers.cs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,98 @@ static TProperty FallbackAccessor(TSyntax syntax, TArg argument)
394394
return expression.Compile();
395395
}
396396

397+
internal static Func<TSyntax, TArg1, TArg2, TProperty> CreateSyntaxPropertyAccessor<TSyntax, TArg1, TArg2, TProperty>(Type type, Type argumentType1, Type argumentType2, string accessorMethodName)
398+
{
399+
static TProperty FallbackAccessor(TSyntax syntax, TArg1 argument1, TArg2 argument2)
400+
{
401+
if (syntax == null)
402+
{
403+
// Unlike an extension method which would throw ArgumentNullException here, the light-up
404+
// behavior needs to match behavior of the underlying property.
405+
throw new NullReferenceException();
406+
}
407+
408+
return default;
409+
}
410+
411+
if (type == null)
412+
{
413+
return FallbackAccessor;
414+
}
415+
416+
if (!typeof(TSyntax).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()))
417+
{
418+
throw new InvalidOperationException();
419+
}
420+
421+
if (!typeof(TArg1).GetTypeInfo().IsAssignableFrom(argumentType1.GetTypeInfo()))
422+
{
423+
throw new InvalidOperationException();
424+
}
425+
426+
if (!typeof(TArg2).GetTypeInfo().IsAssignableFrom(argumentType2.GetTypeInfo()))
427+
{
428+
throw new InvalidOperationException();
429+
}
430+
431+
var methods = type.GetTypeInfo().GetDeclaredMethods(accessorMethodName);
432+
MethodInfo method = null;
433+
foreach (var candidate in methods)
434+
{
435+
var parameters = candidate.GetParameters();
436+
if (parameters.Length != 2)
437+
{
438+
continue;
439+
}
440+
441+
if (!Equals(argumentType1, parameters[0].ParameterType))
442+
{
443+
continue;
444+
}
445+
446+
if (!Equals(argumentType2, parameters[1].ParameterType))
447+
{
448+
continue;
449+
}
450+
451+
method = candidate;
452+
}
453+
454+
if (method == null)
455+
{
456+
return FallbackAccessor;
457+
}
458+
459+
if (!typeof(TProperty).GetTypeInfo().IsAssignableFrom(method.ReturnType.GetTypeInfo()))
460+
{
461+
throw new InvalidOperationException();
462+
}
463+
464+
var syntaxParameter = Expression.Parameter(typeof(TSyntax), "syntax");
465+
var arg1Parameter = Expression.Parameter(typeof(TArg1), "arg1");
466+
var arg2Parameter = Expression.Parameter(typeof(TArg2), "arg2");
467+
Expression instance =
468+
type.GetTypeInfo().IsAssignableFrom(typeof(TSyntax).GetTypeInfo())
469+
? (Expression)syntaxParameter
470+
: Expression.Convert(syntaxParameter, type);
471+
Expression argument1 =
472+
argumentType1.GetTypeInfo().IsAssignableFrom(typeof(TArg1).GetTypeInfo())
473+
? (Expression)arg1Parameter
474+
: Expression.Convert(arg1Parameter, argumentType1);
475+
Expression argument2 =
476+
argumentType2.GetTypeInfo().IsAssignableFrom(typeof(TArg2).GetTypeInfo())
477+
? (Expression)arg2Parameter
478+
: Expression.Convert(arg2Parameter, argumentType2);
479+
480+
Expression<Func<TSyntax, TArg1, TArg2, TProperty>> expression =
481+
Expression.Lambda<Func<TSyntax, TArg1, TArg2, TProperty>>(
482+
Expression.Call(instance, method, argument1, argument2),
483+
syntaxParameter,
484+
arg1Parameter,
485+
arg2Parameter);
486+
return expression.Compile();
487+
}
488+
397489
internal static TryGetValueAccessor<TSyntax, TKey, TValue> CreateTryGetValueAccessor<TSyntax, TKey, TValue>(Type type, Type keyType, string methodName)
398490
{
399491
static bool FallbackAccessor(TSyntax syntax, TKey key, out TValue value)

0 commit comments

Comments
 (0)