Skip to content

Commit

Permalink
Add support for the constructors of attributes
Browse files Browse the repository at this point in the history
Closes #5
  • Loading branch information
mykolav committed Feb 19, 2023
1 parent 06d049d commit f3d60bc
Show file tree
Hide file tree
Showing 11 changed files with 337 additions and 167 deletions.
70 changes: 58 additions & 12 deletions RequireNamedArgs.Tests/Analyzer/AnalyzerTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand All @@ -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) {}
Expand All @@ -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) {}
Expand All @@ -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) {}
Expand All @@ -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
{
Expand All @@ -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
{
Expand All @@ -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
{
Expand All @@ -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
{
Expand All @@ -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
{
Expand All @@ -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
{
Expand All @@ -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
{
Expand All @@ -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
}
]
43 changes: 42 additions & 1 deletion RequireNamedArgs.Tests/CodeFix/CodeFixProviderTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}]
]
4 changes: 2 additions & 2 deletions RequireNamedArgs/Analysis/ArgumentAndParameter.fs
Original file line number Diff line number Diff line change
@@ -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 }
106 changes: 106 additions & 0 deletions RequireNamedArgs/Analysis/ArgumentSyntaxInfo.fs
Original file line number Diff line number Diff line change
@@ -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<ArgumentSyntaxInfo>): 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 -> [||]
Loading

0 comments on commit f3d60bc

Please sign in to comment.