Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docfx/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#Primary settings apply to all files unless overridden below
root = true

[*]
indent_style = space
indent_size = 4
insert_final_newline = true
tab_width = 4
end_of_line = crlf

# VSSPELL: Spell checker settings for all files
vsspell_section_id = 869f688c36354e788f0a0b53ab42cf41

# [VSSpellChecker bug 277](https://github.com/EWSoftware/VSSpellChecker/issues/277)
# VSSPELL: Disable the analyzers until Bug 277 is fixed.
vsspell_code_analyzers_enabled = false
vsspell_ignored_words_869f688c36354e788f0a0b53ab42cf41 = File:.\IgnoredWords.dic

# match VS generated formatting for MSBuild project files
[*.*proj,*.props,*.targets]
indent_style = space
indent_size = 2
tab_width = 2

[*.yml,*.yaml]
indent_style = space
indent_size = 2
tab_width = 2

[*.md]
# mark left margin for split screen preview of markdown files
# requires: https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelinesPreview
guidelines = 92

6 changes: 6 additions & 0 deletions docfx/CommandLine/Diagnostics/UNC000.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# UNC0000: An internal analyzer exception occurred.
An internal error occurred in the analyzer. Please [report](https://github.com/UbiquityDotNET/Ubiquity.NET.Utils/issues)
the issue with as much detail as possible, ideally with a small repro to help identify the
problem. Please include the full stack of the exception as shown in the message details.
Occurrences of this diagnostic are ALWAYS a bug in the generator/Analyzer in question and
should be fixed.
38 changes: 38 additions & 0 deletions docfx/CommandLine/Diagnostics/UNC001.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# UNC001: Missing command attribute on containing type
A command line property annotating attribute is applied to a property but the containing
type does not contain an attribute that designates it as a command. The generator will
***ignore*** the property and the ***entire*** class.

## Example:
``` C#
using System.IO;

using Ubiquity.NET.CommandLine.GeneratorAttributes;

namespace TestNamespace;

internal class testInput1
{
[Option( "-o" )] // UNC001 reported here
public required DirectoryInfo SomePath { get; init; }
}
```

## Fix:
``` C#
using System.IO;

using Ubiquity.NET.CommandLine.GeneratorAttributes;

namespace TestNamespace;

// apply a command attribute to the type to allow generation
// of the backing details for parsing and binding the results to this type.
[RootCommand( Description = "Root command for tests" )]
internal class testInput1
{
[Option( "-o" )]
public required DirectoryInfo SomePath { get; init; }
}
```

45 changes: 45 additions & 0 deletions docfx/CommandLine/Diagnostics/UNC002.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# UNC002 : Property attribute not allowed standalone
This diagnostic is reported when an attribute is detected that is a qualifier to another
required attribute, but the required attribute is not present.

## Example
`FolderValidationAttribute` provides additional information for validation of a property. To
generate source that operates correctly an additional attribute is required. For example,
to specify that a Folder provided must exist already a command can apply the
`FolderValidationAttribute`. However that attribute requires additional information to
identify the property as an option and other behaviors. Without the constrained attribute
this attribute and property it is attached to is ignored by source generation.

``` C#
using System.IO;

using Ubiquity.NET.CommandLine.GeneratorAttributes;

namespace TestNamespace;

[RootCommand( Description = "Root command for tests" )]
internal class testInput1
{
// This attribute triggers UNC002 - Property attribute FolderValidation is not allowed on a property independent of a qualifying attribute such as OptionAttribute.
[FolderValidation( FolderValidation.CreateIfNotExist )]
public required DirectoryInfo SomePath { get; init; }
}
```

## Fix
``` C#
using System.IO;

using Ubiquity.NET.CommandLine.GeneratorAttributes;

namespace TestNamespace;

[RootCommand( Description = "Root command for tests" )]
internal class testInput1
{
// Fix is to apply the attribute that indicates the property being validated
[Option( "-o", Description = "Test SomePath" )]
[FolderValidation( FolderValidation.CreateIfNotExist )]
public required DirectoryInfo SomePath { get; init; }
}
```
24 changes: 24 additions & 0 deletions docfx/CommandLine/Diagnostics/UNC003.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# UNC003: Property has incorrect type for attribute
The property's type is not consistent with the requirements of the attribute applied to it.
Some attributes (especially validation annotations) require a particular type for the
property to generate valid code. When this diagnostic is raised it is an indication that an
incorrect type is used. If this is ignored or suppressed the property is ignored for code
generation.

## Example
``` C#
using System.IO;

using Ubiquity.NET.CommandLine.GeneratorAttributes;

namespace TestNamespace;

[RootCommand( Description = "Root command for tests" )]
internal class testInput1
{
[Option( "-o", Description = "Test SomePath" )]
// This attribute triggers UNC003 - Property attribute 'FileValidation' requires a property of type 'FileInfo'.
[FileValidation( FileValidation.ExistingOnly )]
public required string SomePath { get; init; }
}
```
26 changes: 26 additions & 0 deletions docfx/CommandLine/Diagnostics/UNC004.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# UNC004 : Property type is nullable but marked as required.
This diagnostic is reported when a nullable type is marked as `Required`. This usually
indicates an error in the source applying the attributes. An explicitly annotated nullable
type has a legit value of null, therefore marking it as `Required` makes no sense. Required
means that it is validated as specified on the command line, this validation only occurs at
the time of ***invoking*** the command. (If a different command is parsed from the command
line arguments then no validation occurs).


## Example
``` C#
using Ubiquity.NET.CommandLine.GeneratorAttributes;

namespace TestNamespace;

[RootCommand( Description = "Root command for tests" )]
internal partial class TestOptions
{
// Use of `Required = true` reports diagnostic; UNC004 : Property type is nullable but marked as required.
[Option( "--thing1", Aliases = [ "-t" ], Required = true, Description = "Test Thing1", HelpName = "Help name for thing1" )]
public bool? Thing1 { get; init; }

[Option( "--thing2", Aliases = [ "-t" ], Description = "Test Thing2", HelpName = "Help name for thing2" )]
public bool Thing2 { get; init; }
}
```
15 changes: 14 additions & 1 deletion docfx/CommandLine/index.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# About
Ubiquity.NET.CommandLine contains general extensions for .NET. to support command line
parsing using `System.CommandLine`
parsing using `System.CommandLine`.

A source generator is included that will generate the boilerplate code for command line
parsing and binding. Additionally an analyzer is provided to aid in identifying problems
with usage of the attributes for generation.

## Analyzer Diagnostics
Rule ID | Title |
--------|-------|
[UNC000](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC000.html) | An internal analyzer exception occurred. |
[UNC001](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC001.html) | Missing command attribute on containing type. |
[UNC002](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC002.html) | Property attribute not allowed standalone. |
[UNC003](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC003.html) | Property has incorrect type for attribute. |
[UNC004](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC004.html) | Property type is nullable but marked as required. |
1 change: 1 addition & 0 deletions docfx/IgnoredWords.dic
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nullable
1 change: 1 addition & 0 deletions docfx/documentation.msbuildproj
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
<Target Name="AlwaysRun" BeforeTargets="AfterBuild">
<Message Importance="High" Text="NOTE: Building $(MSBuildProjectFile) does NOTHING, docs are built using the docfx tool. This project is simply a convenient placeholder for organizing/editing files" />
</Target>

<!--
Target to generate the versioning JSON file for consumption by the docs scripts. This target is explicitly called out
by the build scripts to generate the JSON file with all the version details from Ubiquity.NET.Versioning.Build.Tasks.
Expand Down
10 changes: 9 additions & 1 deletion src/Ubiquity.NET.CodeAnalysis.Utils/TypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public static class TypeSymbolExtensions
/// <summary>Gets the full name of the symbol (Including namespace names)</summary>
/// <param name="self">Symbol to get the name from</param>
/// <returns>Enumerable collection of the name parts with outer most namespace first</returns>
public static IEnumerable<string> GetFullName( this ITypeSymbol self )
public static IEnumerable<string> GetFullNameParts( this ITypeSymbol self )
{
foreach(string namespacePart in GetNamespaceNames( self ))
{
Expand All @@ -19,6 +19,14 @@ public static IEnumerable<string> GetFullName( this ITypeSymbol self )
yield return self.Name;
}

/// <summary>Gets the fill name (including namespaces) of a <see cref="ITypeSymbol"/></summary>
/// <param name="self"><see cref="ITypeSymbol"/> to get the name from</param>
/// <returns>full name for the type</returns>
public static string GetFullName(this ITypeSymbol self)
{
return string.Join(".", GetFullNameParts(self));
}

/// <summary>Gets the sequence of namespaces names of the symbol (outermost to innermost)</summary>
/// <param name="self">Symbol to get the name from</param>
/// <returns>Enumerable collection of the name parts with outer most namespace first</returns>
Expand Down
76 changes: 67 additions & 9 deletions src/Ubiquity.NET.CommandLine.SrcGen.UT/CommandAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ public async Task Option_attribute_without_command_triggers_diagnostic( TestRunt
// (9,6): warning UNC001: Property attribute OptionAttribute is only allowed on a property in a type attributed with a command attribute. This use will be ignored by the generator.
analyzerTest.ExpectedDiagnostics.AddRange(
[
new DiagnosticResult("UNC001", DiagnosticSeverity.Warning).WithLocation(9, 6),
new DiagnosticResult("UNC001", DiagnosticSeverity.Error)
.WithSpan(9, 6, 9, 93)
.WithArguments("OptionAttribute"),
]
);

Expand All @@ -43,12 +45,17 @@ public async Task FileValidation_attribute_without_command_triggers_diagnostic(
SourceText txt = GetSourceText( nameof(FileValidation_attribute_without_command_triggers_diagnostic), "input.cs" );
var analyzerTest = CreateTestRunner( txt, testRuntime );

// (9,6): warning UNC001: Property attribute OptionAttribute is only allowed on a property in a type attributed with a command attribute. This use will be ignored by the generator.
// (10,6): warning UNC001: Property attribute FileValidationAttribute is only allowed on a property in a type attributed with a command attribute. This use will be ignored by the generator.
// (10,6): error UNC002: Property attribute FileValidationAttribute is not allowed on a property independent of a qualifying attribute such as OptionAttribute.
analyzerTest.ExpectedDiagnostics.AddRange(
[
new DiagnosticResult("UNC001", DiagnosticSeverity.Warning).WithLocation(10, 6),
new DiagnosticResult("UNC002", DiagnosticSeverity.Error).WithLocation(10, 6),
new DiagnosticResult("UNC001", DiagnosticSeverity.Error)
.WithArguments("OptionAttribute")
.WithSpan(9, 6, 9, 51),

new DiagnosticResult("UNC001", DiagnosticSeverity.Error)
.WithArguments("FileValidationAttribute")
.WithSpan(10, 6, 10, 51),
]
);

Expand All @@ -63,14 +70,19 @@ public async Task FolderValidation_attribute_without_command_triggers_diagnostic
SourceText txt = GetSourceText( nameof(FolderValidation_attribute_without_command_triggers_diagnostic), "input.cs" );
var analyzerTest = CreateTestRunner( txt, testRuntime );

// (10,6): warning UNC001: Property attribute FolderValidationAttribute is only allowed on a property in a type attributed with a command attribute. This use will be ignored by the generator.
// (10,6): error UNC002: Property attribute FolderValidationAttribute is not allowed on a property independent of a qualifying attribute such as OptionAttribute.
// (11,6): error UNC001: Property attribute FolderValidationAttribute is only allowed on a property in a type attributed with a command attribute. This use will be ignored by the generator.
// (11,6): error UNC002: Property attribute FolderValidationAttribute is not allowed on a property independent of a qualifying attribute such as OptionAttribute.
analyzerTest.ExpectedDiagnostics.AddRange(
[
new DiagnosticResult("UNC001", DiagnosticSeverity.Warning).WithLocation(10, 6),
new DiagnosticResult("UNC002", DiagnosticSeverity.Error).WithLocation(10, 6),
new DiagnosticResult("UNC001", DiagnosticSeverity.Error)
.WithSpan(11, 6, 11, 55)
.WithArguments("FolderValidationAttribute"),

new DiagnosticResult("UNC002", DiagnosticSeverity.Error)
.WithSpan(11, 6, 11, 55)
.WithArguments("FolderValidationAttribute"),
]
);
);

await analyzerTest.RunAsync( TestContext.CancellationToken );
}
Expand All @@ -86,6 +98,45 @@ public async Task GoldenPath_produces_no_diagnostics( TestRuntime testRuntime )
await analyzerTest.RunAsync( TestContext.CancellationToken );
}

[TestMethod]
[DataRow( TestRuntime.Net8_0 )]
[DataRow( TestRuntime.Net10_0 )]
public async Task FileValidation_with_wrong_type_produces_UNC002( TestRuntime testRuntime )
{
SourceText txt = GetSourceText( nameof(FileValidation_with_wrong_type_produces_UNC002), "input.cs" );
var analyzerTest = CreateTestRunner( txt, testRuntime );

// (9,6): error UNC003: Property attribute '{0}' requires a property of type '{1}'.
analyzerTest.ExpectedDiagnostics.AddRange(
[
new DiagnosticResult("UNC003", DiagnosticSeverity.Error)
.WithSpan(9, 6, 9, 51),
]
);

await analyzerTest.RunAsync( TestContext.CancellationToken );
}

[TestMethod]
[DataRow( TestRuntime.Net8_0 )]
[DataRow( TestRuntime.Net10_0 )]
public async Task Required_nullable_types_produce_diagnostic( TestRuntime testRuntime )
{
SourceText txt = GetSourceText( nameof(Required_nullable_types_produce_diagnostic), "input.cs" );
var analyzerTest = CreateTestRunner( txt, testRuntime );

// (9,6): error UNC003: Property attribute '{0}' requires a property of type '{1}'.
analyzerTest.ExpectedDiagnostics.AddRange(
[
new DiagnosticResult("UNC004", DiagnosticSeverity.Warning)
.WithSpan(9, 6, 9, 127)
.WithArguments("bool?", "OptionAttribute"),
]
);

await analyzerTest.RunAsync( TestContext.CancellationToken );
}

// TODO: Test that a nullable value is not marked as required. (That's a conflicting claim, if it's required it can't be null)
// A nullable type MAY have a default value handler to provide a null default. Additional test - anything with a default
// value provider shouldn't be "required" it's also nonsensical.
Expand Down Expand Up @@ -117,6 +168,13 @@ private AnalyzerTest<MsTestVerifier> CreateTestRunner( SourceText source, TestRu
};
}

// Sadly, at this time the test infrastructure doesn't provide support for
// testing the help URI
//private static string FormatHelpUri( string id )
//{
// return $"https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/{id}.html";
//}

private static SourceText GetSourceText( params string[] nameParts )
{
return TestHelpers.GetTestText( nameof( CommandAnalyzerTests ), nameParts );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace TestNamespace;

internal class testInput1
{
// This attribute alone should trigger UNC002 - Property attribute FileValidation is not allowed on a property independent of a qualifying attribute such as OptionAttribute.
[FileValidation( FileValidation.ExistingOnly )]
[Option( "-o", Description = "Test SomePath" )]
[FileValidation( FileValidation.ExistingOnly )] // UNC002 : Property attribute not allowed standalone
public required FileInfo SomePath { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Ubiquity.NET.CommandLine.GeneratorAttributes;

namespace TestNamespace;

[RootCommand( Description = "Root command for tests" )]
internal class testInput1
{
[Option( "-o", Description = "Test SomePath" )]
[FileValidation( FileValidation.ExistingOnly )] // UNC003 - Property attribute 'FileValidation' requires a property of type 'FileInfo'.
public required string SomePath { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ namespace TestNamespace;

internal class testInput1
{
// This attribute alone should trigger UNC002 - Property attribute FileValidation is not allowed on a property independent of a qualifying attribute such as OptionAttribute.
[FileValidation( FileValidation.ExistingOnly )]
public required FileInfo SomePath { get; init; }
// UNC001 - Property attribute 'FolderValidation' is only allowed on a property in a type attributed with a command attribute. This use will be ignored by the generator.
// UNC002 - Property attribute 'FolderValidationAttribute' is not allowed on a property independent of a qualifying attribute such as OptionAttribute.
[FolderValidation( FolderValidation.ExistingOnly )]
public required DirectoryInfo SomePath { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Ubiquity.NET.CommandLine.GeneratorAttributes;

namespace TestNamespace;

[RootCommand( Description = "Root command for tests" )]
internal partial class TestOptions
{
// Warning : UNC004 : Type 'bool?' for property 'Thing1' is nullable but marked as required; These annotations conflict resulting in behavior that is explicitly UNDEFINED.
[Option( "--thing1", Aliases = [ "-t" ], Required = true, Description = "Test Thing1", HelpName = "Help name for thing1" )]
public bool? Thing1 { get; init; }

[Option( "--thing2", Aliases = [ "-t" ], Description = "Test Thing2", HelpName = "Help name for thing2" )]
public bool Thing2 { get; init; }
}
Loading
Loading