diff --git a/RequireNamedArgs.Tests/Analyzer/AnalyzerTests.fs b/RequireNamedArgs.Tests/Analyzer/AnalyzerTests.fs index e9313d1..3958a32 100644 --- a/RequireNamedArgs.Tests/Analyzer/AnalyzerTests.fs +++ b/RequireNamedArgs.Tests/Analyzer/AnalyzerTests.fs @@ -65,7 +65,7 @@ module AnalyzerTests = void Bork() { TellPowerLevel(name: ""Goku"", powerLevel: 9001); } ") } - test "Method w/ [RequireNamedArgs] invoked w/ positional args triggers diagnostic" { + test "Method w/ [RequireNamedArgs] invoked w/ positional args triggers the diagnostic" { let testCodeSnippet = (Format.klass @" [RequireNamedArgs] void TellPowerLevel(string name, int powerLevel) {} @@ -78,7 +78,7 @@ module AnalyzerTests = [|expectedDiag|] |> ExpectDiags.toBeEmittedFrom testCodeSnippet } - test "Method w/ [RequireNamedArgs] attribute invoked w/ positional args triggers diagnostic" { + test "Method w/ [RequireNamedArgs] attribute invoked w/ positional args triggers the diagnostic" { let testCodeSnippet = (Format.klass @" [RequireNamedArgs] void TellPowerLevel(string name, int powerLevel) {} @@ -91,7 +91,7 @@ module AnalyzerTests = [|expectedDiag|] |> ExpectDiags.toBeEmittedFrom testCodeSnippet } - test "Static method w/ [RequireNamedArgs] invoked w/ positional args triggers diagnostic" { + test "Static method w/ [RequireNamedArgs] invoked w/ positional args triggers the diagnostic" { let testCodeSnippet = (Format.klass @" [RequireNamedArgs] static void TellPowerLevel(string name, int powerLevel) {} @@ -104,7 +104,7 @@ module AnalyzerTests = [|expectedDiag|] |> ExpectDiags.toBeEmittedFrom testCodeSnippet } - test "Private static method w/ [RequireNamedArgs] invoked w/ positional args triggers diagnostic" { + test "Private static method w/ [RequireNamedArgs] invoked w/ positional args triggers the diagnostic" { let testCodeSnippet = (Format.klass @" [RequireNamedArgs] private static void TellPowerLevel(string name, int powerLevel) {} @@ -118,7 +118,7 @@ module AnalyzerTests = [|expectedDiag|] |> ExpectDiags.toBeEmittedFrom testCodeSnippet } // See https://github.com/mykolav/require-named-args-fs/issues/1 - test "Extension method w/o [RequireNamedArgs] does not triggers diagnostic" { + test "Extension method w/o [RequireNamedArgs] does not triggers the diagnostic" { ExpectDiags.emptyDiagnostics @" static class PowerLevelExtensions { @@ -131,7 +131,7 @@ module AnalyzerTests = } " } - test "Extension method w/ [RequireNamedArgs] invoked w/ named args does not trigger diagnostic" { + test "Extension method w/ [RequireNamedArgs] invoked w/ named args does not trigger the diagnostic" { ExpectDiags.emptyDiagnostics @" static class PowerLevelExtensions { @@ -145,7 +145,7 @@ module AnalyzerTests = } " } - test "Extension method w/ [RequireNamedArgs] invoked w/ positional args triggers diagnostic" { + test "Extension method w/ [RequireNamedArgs] invoked w/ positional args triggers the diagnostic" { let testCodeSnippet = @" static class PowerLevelExtensions { @@ -165,7 +165,7 @@ module AnalyzerTests = [| expectedDiag |] |> ExpectDiags.toBeEmittedFrom testCodeSnippet } - test "Constructor w/ [RequireNamedArgs] invoked w/ named args does not trigger diagnostic" { + test "Constructor w/ [RequireNamedArgs] invoked w/ named args does not trigger the diagnostic" { ExpectDiags.emptyDiagnostics @" class Wombat { @@ -182,7 +182,7 @@ module AnalyzerTests = } " } - test "Constructor w/ [RequireNamedArgs] invoked w/ positional args triggers diagnostic" { + test "Constructor w/ [RequireNamedArgs] invoked w/ positional args triggers the diagnostic" { let testCodeSnippet = @" class Wombat { @@ -204,7 +204,7 @@ module AnalyzerTests = [| expectedDiag |] |> ExpectDiags.toBeEmittedFrom testCodeSnippet } - test "Constructor w/ [RequireNamedArgs] invoked implicitly w/ named args does not trigger diagnostic" { + test "Constructor w/ [RequireNamedArgs] invoked implicitly w/ named args does not trigger the diagnostic" { ExpectDiags.emptyDiagnostics @" class Wombat { @@ -221,7 +221,7 @@ module AnalyzerTests = } " } - test "Constructor w/ [RequireNamedArgs] invoked implicitly w/ positional args triggers diagnostic" { + test "Constructor w/ [RequireNamedArgs] invoked implicitly w/ positional args triggers the diagnostic" { let testCodeSnippet = @" class Wombat { @@ -242,4 +242,50 @@ module AnalyzerTests = fileName="Test0.cs", line=15u, column=36u) [| expectedDiag |] |> ExpectDiags.toBeEmittedFrom testCodeSnippet - }] + } + test "Attribute constructor w/ [RequireNamedArgs] invoked w/ named args does not trigger the diagnostic" { + ExpectDiags.emptyDiagnostics @" + [PowerLevel(name: ""Goku"", powerLevel: 9001)] + class Goku { } + + [AttributeUsage(AttributeTargets.Class)] + public sealed class PowerLevelAttribute : Attribute + { + public string Name { get; } + public int PowerLevel { get; } + + [RequireNamedArgs] + public PowerLevelAttribute(string name, int powerLevel) + { + Name = name; + PowerLevel = powerLevel; + } + } + " + } + test "Attribute constructor w/ [RequireNamedArgs] invoked w/ positional args triggers the diagnostic" { + let testCodeSnippet = @" + [PowerLevel(""Goku"", 9001)] + class Goku { } + + [AttributeUsage(AttributeTargets.Class)] + public sealed class PowerLevelAttribute : Attribute + { + public string Name { get; } + public int PowerLevel { get; } + + [RequireNamedArgs] + public PowerLevelAttribute(string name, int powerLevel) + { + Name = name; + PowerLevel = powerLevel; + } + } + " + let expectedDiag = RequireNamedArgsDiagResult.Create(invokedMethod=".ctor", + paramNamesByType=[[ "line"; "column" ]], + fileName="Test0.cs", line=5u, column=22u) + + [| expectedDiag |] |> ExpectDiags.toBeEmittedFrom testCodeSnippet + } + ] diff --git a/RequireNamedArgs.Tests/CodeFix/CodeFixProviderTests.fs b/RequireNamedArgs.Tests/CodeFix/CodeFixProviderTests.fs index b215115..0d53d94 100644 --- a/RequireNamedArgs.Tests/CodeFix/CodeFixProviderTests.fs +++ b/RequireNamedArgs.Tests/CodeFix/CodeFixProviderTests.fs @@ -207,4 +207,45 @@ module CodeFixProviderTests = originalSnippet |> ExpectCode.snippetToBeFixedAndMatch expectedFixedSnippet }] - ] + testList "Attribute constructor w/ [RequireNamedArgs]" [ + test "Invocation w/ positional args is fixed to named args" { + let originalSnippet = @" + [PowerLevel(""Goku"", 9001)] + class Goku { } + + [AttributeUsage(AttributeTargets.Class)] + public sealed class PowerLevelAttribute : Attribute + { + public string Name { get; } + public int PowerLevel { get; } + + [RequireNamedArgs] + public PowerLevelAttribute(string name, int powerLevel) + { + Name = name; + PowerLevel = powerLevel; + } + } + " + let expectedFixedSnippet = @" + [PowerLevel(name: ""Goku"", powerLevel: 9001)] + class Goku { } + + [AttributeUsage(AttributeTargets.Class)] + public sealed class PowerLevelAttribute : Attribute + { + public string Name { get; } + public int PowerLevel { get; } + + [RequireNamedArgs] + public PowerLevelAttribute(string name, int powerLevel) + { + Name = name; + PowerLevel = powerLevel; + } + } + " + + originalSnippet |> ExpectCode.snippetToBeFixedAndMatch expectedFixedSnippet + }] + ] diff --git a/RequireNamedArgs/Analysis/ArgumentAndParameter.fs b/RequireNamedArgs/Analysis/ArgumentAndParameter.fs index 3f58b1d..519740c 100644 --- a/RequireNamedArgs/Analysis/ArgumentAndParameter.fs +++ b/RequireNamedArgs/Analysis/ArgumentAndParameter.fs @@ -1,10 +1,10 @@ module RequireNamedArgs.ArgumentAndParameter -open Microsoft.CodeAnalysis.CSharp.Syntax open Microsoft.CodeAnalysis +open RequireNamedArgs.Analysis type ArgWithParamSymbol = { - ArgSyntax: ArgumentSyntax; + ArgSyntax: ArgumentSyntaxInfo; ParamSymbol: IParameterSymbol } diff --git a/RequireNamedArgs/Analysis/ArgumentSyntaxInfo.fs b/RequireNamedArgs/Analysis/ArgumentSyntaxInfo.fs new file mode 100644 index 0000000..f24a43e --- /dev/null +++ b/RequireNamedArgs/Analysis/ArgumentSyntaxInfo.fs @@ -0,0 +1,106 @@ +namespace RequireNamedArgs.Analysis + + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.CSharp +open Microsoft.CodeAnalysis.CSharp.Syntax + + +type ArgumentSyntaxInfo = + | ArgumentSyntax of ArgumentSyntax + | AttributeArgumentSyntax of AttributeArgumentSyntax with + + member this.NameColon = + match this with + | ArgumentSyntax argSyntax -> argSyntax.NameColon + | AttributeArgumentSyntax attrArgSyntax -> attrArgSyntax.NameColon + + member this.WithNameColon(paramInfo: ParamInfo): ArgumentSyntaxInfo = + match this with + | ArgumentSyntax argSyntax -> + ArgumentSyntax (argSyntax.WithNameColon( + SyntaxFactory.NameColon(paramInfo.ParamSymbol.Name)) + .WithTriviaFrom(argSyntax)) // Preserve whitespaces, etc. from the original code. + | AttributeArgumentSyntax attrArgSyntax -> + AttributeArgumentSyntax (attrArgSyntax.WithNameColon( + SyntaxFactory.NameColon(paramInfo.ParamSymbol.Name)) + .WithTriviaFrom(attrArgSyntax)) // Preserve whitespaces, etc. from the original code. + + +type ArgumentListSyntaxInfo = + | ArgumentListSyntax of ArgumentListSyntax + | AttributeArgumentListSyntax of AttributeArgumentListSyntax with + + + member this.Syntax + with get(): SyntaxNode = + match this with + | ArgumentListSyntax list -> list + | AttributeArgumentListSyntax list -> list + + + member this.Parent = + match this with + | ArgumentListSyntax list -> list.Parent + | AttributeArgumentListSyntax list -> list.Parent + + + member this.Arguments = + match this with + | ArgumentListSyntax list -> list.Arguments |> Seq.map(fun it -> ArgumentSyntax it) + | AttributeArgumentListSyntax list -> list.Arguments |> Seq.map(fun it -> AttributeArgumentSyntax it) + |> Array.ofSeq + + + member this.WithArguments(argumentSyntaxInfos: seq): ArgumentListSyntaxInfo = + match this with + | ArgumentListSyntax list -> + let argumentSyntaxes = argumentSyntaxInfos + |> Seq.choose (fun it -> match it with + | ArgumentSyntax it -> Some it + | _ -> None) + |> Array.ofSeq + if argumentSyntaxes.Length = 0 + then + this + else + + ArgumentListSyntax (list.WithArguments( + SyntaxFactory.SeparatedList( + argumentSyntaxes, + list.Arguments.GetSeparators()))) + | AttributeArgumentListSyntax list -> + let attributeArgumentSyntaxes = argumentSyntaxInfos + |> Seq.choose (fun it -> match it with + | AttributeArgumentSyntax it -> Some it + | _ -> None) + |> Array.ofSeq + if attributeArgumentSyntaxes.Length = 0 + then + this + else + + AttributeArgumentListSyntax (list.WithArguments( + SyntaxFactory.SeparatedList( + attributeArgumentSyntaxes, + list.Arguments.GetSeparators()))) + + + +module ArgumentSyntaxInfo = + + + type SyntaxNode with + member syntaxNode.GetArgumentList(): ArgumentListSyntaxInfo option = + match syntaxNode with + | :? InvocationExpressionSyntax as it -> Some (ArgumentListSyntax it.ArgumentList) + | :? ObjectCreationExpressionSyntax as it -> Some (ArgumentListSyntax it.ArgumentList) + | :? ImplicitObjectCreationExpressionSyntax as it -> Some (ArgumentListSyntax it.ArgumentList) + | :? AttributeSyntax as it -> Some (AttributeArgumentListSyntax it.ArgumentList) + | _ -> None + + + member syntaxNode.GetArguments(): ArgumentSyntaxInfo[] = + match syntaxNode.GetArgumentList() with + | Some info -> info.Arguments + | None -> [||] diff --git a/RequireNamedArgs/Analysis/InvocationAnalyzer.fs b/RequireNamedArgs/Analysis/InvocationAnalyzer.fs index 4c3bc67..f01483f 100644 --- a/RequireNamedArgs/Analysis/InvocationAnalyzer.fs +++ b/RequireNamedArgs/Analysis/InvocationAnalyzer.fs @@ -6,11 +6,12 @@ open Microsoft.CodeAnalysis.CSharp.Syntax open RequireNamedArgs.ArgumentAndParameter open RequireNamedArgs.Support open RequireNamedArgs.Support.CSharpAdapters -open RequireNamedArgs.Analysis.ParamInfo +open RequireNamedArgs.Analysis.ArgumentSyntaxInfo +open RequireNamedArgs.Analysis.ParamExtensions type InvocationAnalyzer private(_sema: SemanticModel, - _exprSyntax: ExpressionSyntax, + _exprSyntax: SyntaxNode, _methodSymbol: IMethodSymbol) = @@ -32,7 +33,7 @@ type InvocationAnalyzer private(_sema: SemanticModel, |> Seq.exists (fun attrData -> attrData.AttributeClass.Name = "RequireNamedArgsAttribute") - let zipArgSyntaxesWithParamSymbols (argSyntaxes: SeparatedSyntaxList) + let zipArgSyntaxesWithParamSymbols (argSyntaxes: ArgumentSyntaxInfo[]) : Res = let argWithParamInfos = argSyntaxes @@ -57,17 +58,15 @@ type InvocationAnalyzer private(_sema: SemanticModel, static member Create(sema: SemanticModel, analyzedSyntaxNode: SyntaxNode): Res = - let analyzedExprSyntaxOpt = analyzedSyntaxNode |> asOptional - if analyzedExprSyntaxOpt.IsNone - then - // If the supplied syntax node doesn't represent an expression, - // it cannot be an invocation. - // As a result we're not interested. - StopAnalysis - else - - let analyzedExprSyntax = analyzedExprSyntaxOpt.Value - let analyzedMethodSymbolOpt = sema.GetSymbolInfo(analyzedExprSyntax).Symbol |> asOptional + let analyzedMethodSymbolOpt = + match analyzedSyntaxNode with + | :? ExpressionSyntax as analyzedExprSyntax -> + sema.GetSymbolInfo(analyzedExprSyntax).Symbol |> asOptional + | :? AttributeSyntax as analyzedAttrSyntax -> + sema.GetSymbolInfo(analyzedAttrSyntax).Symbol |> asOptional + | _ -> + None + if analyzedMethodSymbolOpt.IsNone then // If the symbol that corresponds to the supplied expression syntax is not a method symbol, @@ -75,7 +74,6 @@ type InvocationAnalyzer private(_sema: SemanticModel, // As a result we're not interested. StopAnalysis else - let analyzedMethodSymbol = analyzedMethodSymbolOpt.Value if not (isSupportedMethodKind analyzedMethodSymbol && @@ -88,7 +86,7 @@ type InvocationAnalyzer private(_sema: SemanticModel, else // OK, we're ready to analyze this method invocation/object creation. - Ok (InvocationAnalyzer(sema, analyzedExprSyntax, analyzedMethodSymbol)) + Ok (InvocationAnalyzer(sema, analyzedSyntaxNode, analyzedMethodSymbol)) member this.MethodName: string = _methodSymbol.Name @@ -105,18 +103,15 @@ type InvocationAnalyzer private(_sema: SemanticModel, /// we should stop analysis of the current expression. /// member this.GetArgsMissingNames(): Res = - let argSyntaxes = - match _exprSyntax with - | :? ImplicitObjectCreationExpressionSyntax as it -> it.ArgumentList.Arguments - | _ -> _exprSyntax.GetArguments() + let argSyntaxes = _exprSyntax.GetArguments() - if not (argSyntaxes.Any()) + if argSyntaxes.Length = 0 then Ok NoArgsMissingNames else - let lastArgIndex = argSyntaxes.Count - 1 - let lastParamInfoRes = _sema.GetParameterInfo(_methodSymbol, lastArgIndex, argSyntaxes.[lastArgIndex]) + let lastArgIndex = argSyntaxes.Length - 1 + let lastParamInfoRes = _sema.GetParameterInfo(_methodSymbol, lastArgIndex, argSyntaxes[lastArgIndex]) if lastParamInfoRes.ShouldStopAnalysis then StopAnalysis diff --git a/RequireNamedArgs/Analysis/ParamExtensiosn.fs b/RequireNamedArgs/Analysis/ParamExtensiosn.fs new file mode 100644 index 0000000..c3039e8 --- /dev/null +++ b/RequireNamedArgs/Analysis/ParamExtensiosn.fs @@ -0,0 +1,89 @@ +namespace RequireNamedArgs.Analysis + + +open System +open System.Collections.Immutable +open Microsoft.CodeAnalysis +open RequireNamedArgs.Support + + +module ParamExtensions = + + + type ISymbol with + member symbol.GetParameters(): ImmutableArray = + match symbol with + | :? IMethodSymbol as s -> s.Parameters + | :? IPropertySymbol as s -> s.Parameters + | _ -> ImmutableArray.Empty + + + /// + /// To be able to convert positional arguments to named we need to find + /// corresponding for each argument. + /// + type SemanticModel with + member sema.GetParameterInfo (methodOrPropertySymbol: ISymbol, + argIndex: int, + argSyntax: ArgumentSyntaxInfo) + : Res = + let paramSymbols = methodOrPropertySymbol.GetParameters() + if paramSymbols.IsEmpty + then + // We have an ArgumentSyntax but the corresponding method + // doesn't take any parameters. + // Looks like a compile error in the analyzed invocation: + // it passes an argument to a method that doesn't take any. + // We pass up on analyzing this invocation, compiler will emit a diagnostic about it. + StopAnalysis + else + + if isNull argSyntax.NameColon + then + // + // We found a positional argument. + // + if argIndex >= 0 && argIndex < paramSymbols.Length + then + Ok { MethodOrPropertySymbol = methodOrPropertySymbol; + ParamSymbol = paramSymbols[argIndex] } + else + + if argIndex >= paramSymbols.Length && + paramSymbols[paramSymbols.Length - 1].IsParams + then + Ok { MethodOrPropertySymbol = methodOrPropertySymbol; + ParamSymbol = paramSymbols[paramSymbols.Length - 1] } + else + + StopAnalysis + else + + // + // Potentially, we found a named argument. + // + if (isNull argSyntax.NameColon.Name) || + (isNull argSyntax.NameColon.Name.Identifier.ValueText) + then + // We encountered an argument in the analyzed invocation, + // that we don't know how to handle. + // Pass up on analyzing this invocation. + // (How can `NameColon.Name` or `NameColon.Name.Identifier.ValueText` actually be null?) + StopAnalysis + else + + // Yes, it's a named argument. + let paramName = argSyntax.NameColon.Name.Identifier.ValueText + let parameterOpt = + paramSymbols + |> Seq.tryFind (fun param -> String.Equals(param.Name, paramName, StringComparison.Ordinal)) + + match parameterOpt with + | Some parameter -> + Ok { MethodOrPropertySymbol = methodOrPropertySymbol; + ParamSymbol = parameter } + | None -> + // We could not find a parameter with the name matching the argument's name. + // Looks like a compile error in the analyzed invocation: it's using a wrong name to name an argument. + // We pass up on analyzing this invocation, compiler will emit a diagnostic about it. + StopAnalysis diff --git a/RequireNamedArgs/Analysis/ParamInfo.fs b/RequireNamedArgs/Analysis/ParamInfo.fs index b749a46..ad2888f 100644 --- a/RequireNamedArgs/Analysis/ParamInfo.fs +++ b/RequireNamedArgs/Analysis/ParamInfo.fs @@ -1,109 +1,9 @@ namespace RequireNamedArgs.Analysis -open System -open System.Collections.Immutable open Microsoft.CodeAnalysis -open Microsoft.CodeAnalysis.CSharp.Syntax -open RequireNamedArgs.Support type ParamInfo = { MethodOrPropertySymbol : ISymbol ParamSymbol : IParameterSymbol } - - -module ParamInfo = - - - type ISymbol with - member symbol.GetParameters(): ImmutableArray = - match symbol with - | :? IMethodSymbol as s -> s.Parameters - | :? IPropertySymbol as s -> s.Parameters - | _ -> ImmutableArray.Empty - - - type ExpressionSyntax with - member exprSyntax.GetArgumentList(): ArgumentListSyntax option = - match exprSyntax with - | :? InvocationExpressionSyntax as i -> Some i.ArgumentList - | :? ObjectCreationExpressionSyntax as o -> Some o.ArgumentList - | _ -> None - - - member exprSyntax.GetArguments(): SeparatedSyntaxList = - match exprSyntax.GetArgumentList() with - | Some argList -> argList.Arguments - | None -> SeparatedSyntaxList() - - - /// - /// To be able to convert positional arguments to named we need to find - /// corresponding for each argument. - /// - type SemanticModel with - member sema.GetParameterInfo (methodOrPropertySymbol: ISymbol, - argIndex: int, - argSyntax: ArgumentSyntax) - : Res = - let paramSymbols = methodOrPropertySymbol.GetParameters() - if paramSymbols.IsEmpty - then - // We have an ArgumentSyntax but the corresponding method - // doesn't take any parameters. - // Looks like a compile error in the analyzed invocation: - // it passes an argument to a method that doesn't take any. - // We pass up on analyzing this invocation, compiler will emit a diagnostic about it. - StopAnalysis - else - - if isNull argSyntax.NameColon - then - // - // We found a positional argument. - // - if argIndex >= 0 && argIndex < paramSymbols.Length - then - Ok { MethodOrPropertySymbol = methodOrPropertySymbol; - ParamSymbol = paramSymbols.[argIndex] } - else - - if argIndex >= paramSymbols.Length && - paramSymbols.[paramSymbols.Length - 1].IsParams - then - Ok { MethodOrPropertySymbol = methodOrPropertySymbol; - ParamSymbol = paramSymbols.[paramSymbols.Length - 1] } - else - - StopAnalysis - else - - // - // Potentially, we found a named argument. - // - if (isNull argSyntax.NameColon.Name) || - (isNull argSyntax.NameColon.Name.Identifier.ValueText) - then - // We encountered an argument in the analyzed invocation, - // that we don't know how to handle. - // Pass up on analyzing this invocation. - // (How can `NameColon.Name` or `NameColon.Name.Identifier.ValueText` actually be null?) - StopAnalysis - else - - // Yes, it's a named argument. - let paramName = argSyntax.NameColon.Name.Identifier.ValueText - let parameterOpt = - paramSymbols - |> Seq.tryFind (fun param -> String.Equals(param.Name, paramName, StringComparison.Ordinal)) - - match parameterOpt with - | Some parameter -> - Ok { MethodOrPropertySymbol = methodOrPropertySymbol; - ParamSymbol = parameter } - | None -> - // We could not find a parameter with the name matching the argument's name. - // Looks like a compile error in the analyzed invocation: it's using a wrong name to name an argument. - // We pass up on analyzing this invocation, compiler will emit a diagnostic about it. - StopAnalysis diff --git a/RequireNamedArgs/RequireNamedArgs.fsproj b/RequireNamedArgs/RequireNamedArgs.fsproj index 5975e03..46e065c 100644 --- a/RequireNamedArgs/RequireNamedArgs.fsproj +++ b/RequireNamedArgs/RequireNamedArgs.fsproj @@ -11,6 +11,8 @@ + + diff --git a/RequireNamedArgs/RequireNamedArgs.nuspec b/RequireNamedArgs/RequireNamedArgs.nuspec index b39869c..4e820dd 100644 --- a/RequireNamedArgs/RequireNamedArgs.nuspec +++ b/RequireNamedArgs/RequireNamedArgs.nuspec @@ -2,7 +2,7 @@ RequireNamedArgs - 0.0.5 + 0.0.6 Mykola Musiienko Mykola Musiienko false @@ -10,10 +10,10 @@ https://github.com/mykolav/require-named-args-fs icon.png "Require named arguments" Roslyn code analyzer and code-fix provider for C# - Copyright © Mykola Musiienko 2021 + Copyright © Mykola Musiienko 2023 roslyn roslyn-analyzer - Jeff Ward (https://github.com/fuzzybinary) contributed support for constructors + Added support for the implicit object creation syntax and an attribute's constructors diff --git a/RequireNamedArgs/RequireNamedArgsAnalyzer.fs b/RequireNamedArgs/RequireNamedArgsAnalyzer.fs index 0f8ad64..c707f41 100644 --- a/RequireNamedArgs/RequireNamedArgsAnalyzer.fs +++ b/RequireNamedArgs/RequireNamedArgsAnalyzer.fs @@ -51,7 +51,8 @@ type public RequireNamedArgsAnalyzer() = (fun c -> this.Analyze c), SyntaxKind.InvocationExpression, SyntaxKind.ObjectCreationExpression, - SyntaxKind.ImplicitObjectCreationExpression) + SyntaxKind.ImplicitObjectCreationExpression, + SyntaxKind.Attribute) member private this.Analyze(context: SyntaxNodeAnalysisContext) = diff --git a/RequireNamedArgs/RequireNamedArgsCodeFixProvider.fs b/RequireNamedArgs/RequireNamedArgsCodeFixProvider.fs index a6d5f50..02698f4 100644 --- a/RequireNamedArgs/RequireNamedArgsCodeFixProvider.fs +++ b/RequireNamedArgs/RequireNamedArgsCodeFixProvider.fs @@ -8,11 +8,11 @@ open System.Threading.Tasks open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.CodeActions open Microsoft.CodeAnalysis.CodeFixes -open Microsoft.CodeAnalysis.CSharp -open Microsoft.CodeAnalysis.CSharp.Syntax open FSharp.Control.Tasks.V2.ContextInsensitive +open RequireNamedArgs.Analysis open RequireNamedArgs.Analyzer -open RequireNamedArgs.Analysis.ParamInfo +open RequireNamedArgs.Analysis.ArgumentSyntaxInfo +open RequireNamedArgs.Analysis.ParamExtensions open RequireNamedArgs.Support @@ -37,9 +37,9 @@ type public RequireNamedArgsCodeFixProvider() = override this.RegisterCodeFixesAsync(context) = (task { let! root = context.Document.GetSyntaxRootAsync(context.CancellationToken) - let diagnostic = context.Diagnostics |> Seq.head; - let diagnosticSpan = diagnostic.Location.SourceSpan; - let exprSyntax = root.FindNode(diagnosticSpan) :?> ExpressionSyntax; + let diagnostic = context.Diagnostics |> Seq.head + let diagnosticSpan = diagnostic.Location.SourceSpan + let syntaxNode = root.FindNode(diagnosticSpan) // Register a code action that will invoke the fix. context.RegisterCodeFix( @@ -49,7 +49,7 @@ type public RequireNamedArgsCodeFixProvider() = return! this.PrefixArgsWithNamesAsync( context.Document, root, - exprSyntax, + syntaxNode, cancellationToken) }, equivalenceKey = title), @@ -60,37 +60,27 @@ type public RequireNamedArgsCodeFixProvider() = member private this.PrefixArgsWithNamesAsync(document: Document, root: SyntaxNode, - exprSyntax: ExpressionSyntax, + syntaxNode: SyntaxNode, cancellationToken: CancellationToken) = task { let! sema = document.GetSemanticModelAsync(cancellationToken) // As it's the named args code fix provider, // the analyzer has already checked that it's OK to make these args named. - match exprSyntax.GetArgumentList() with + match syntaxNode.GetArgumentList() with | Some originalArgListSyntax -> - let exprSyntax = originalArgListSyntax.Parent :?> ExpressionSyntax - let methodOrPropertySymbol = sema.GetSymbolInfo(exprSyntax).Symbol + let methodOrPropertySymbol = sema.GetSymbolInfo(originalArgListSyntax.Parent).Symbol - let withNameColon (argIndex: int) (argSyntax: ArgumentSyntax) = + let withNameColon (argIndex: int) (argSyntax: ArgumentSyntaxInfo) = match sema.GetParameterInfo(methodOrPropertySymbol, argIndex, argSyntax) with - | Ok paramInfo -> - argSyntax.WithNameColon( - SyntaxFactory.NameColon(paramInfo.ParamSymbol.Name)) - .WithTriviaFrom(argSyntax) // Preserve whitespaces, etc. from the original code. - | _ -> - argSyntax + | Ok paramInfo -> argSyntax.WithNameColon(paramInfo) // Preserve whitespaces, etc. from the original code. + | _ -> argSyntax let namedArgSyntaxes = originalArgListSyntax.Arguments |> Seq.mapi withNameColon - - let newArgListSyntax = - originalArgListSyntax.WithArguments( - SyntaxFactory.SeparatedList( - namedArgSyntaxes, - originalArgListSyntax.Arguments.GetSeparators())) + let newArgListSyntax = originalArgListSyntax.WithArguments(namedArgSyntaxes) // An argument list is an "addressable" syntax element, that we can directly // replace in the document's root. - return document.WithSyntaxRoot(root.ReplaceNode(originalArgListSyntax, newArgListSyntax)) + return document.WithSyntaxRoot(root.ReplaceNode(originalArgListSyntax.Syntax, newArgListSyntax.Syntax)) | None -> return document }