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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ This generates `DashboardModel -> DashboardScreen` even though neither type foll
Contract discovery has these constraints:

- each configured contract must be an open generic type with exactly one type parameter;
- invalid configured contracts are ignored and produce the `SVL0006` error;
- the contract may be an interface or a base class, including one inherited indirectly;
- discovered views must be concrete, non-generic types from the current compilation;
- discovered views must derive from `UserControl`, `Window`, or a type configured through `StaticViewLocatorAdditionalViewBaseTypes`;
Expand Down Expand Up @@ -213,6 +214,7 @@ The solution builds the same complete AXAML sample against both distributions. `
| `SVL0003` | Error | More than one inferred view maps to the same view model. |
| `SVL0004` | Error | A mapped view is inaccessible, abstract, or has no accessible constructor callable without arguments. |
| `SVL0005` | Error | The annotated locator is nested, static, file-local, or not partial. |
| `SVL0006` | Error | A configured mapping contract is not an open generic interface or class with exactly one type parameter. |

### Attribute options

Expand Down
29 changes: 29 additions & 0 deletions StaticViewLocator.Tests/StaticViewLocatorGeneratorSnapshotTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,35 @@ namespace StaticViewLocator.Tests;

public class StaticViewLocatorGeneratorSnapshotTests
{
[Fact]
public void ReportsInvalidMappingContracts()
{
const string input = """
using StaticViewLocator;

namespace TestApp;

public interface IInvalidContract { }
public interface IViewFor<TViewModel> { }

[StaticViewLocator(ViewModelMappingContracts = new[]
{
typeof(IInvalidContract),
typeof(IViewFor<string>)
})]
public partial class ViewLocator { }
""";

var result = StaticViewLocatorGeneratorVerifier.RunGenerator(input);
var diagnostics = result.GeneratorDiagnostics
.Where(static item => item.Id == "SVL0006")
.ToArray();

Assert.Equal(2, diagnostics.Length);
Assert.Contains(diagnostics, static item => item.GetMessage().Contains("TestApp.IInvalidContract", StringComparison.Ordinal));
Assert.Contains(diagnostics, static item => item.GetMessage().Contains("TestApp.IViewFor<string>", StringComparison.Ordinal));
}

[Fact]
public async Task GeneratesFactoriesForConstructorsCallableWithoutArguments()
{
Expand Down
1 change: 1 addition & 0 deletions StaticViewLocator/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ SVL0002 | StaticViewLocator.Generation | Error | Source member is incompatible w
SVL0003 | StaticViewLocator.Generation | Error | View mapping is ambiguous
SVL0004 | StaticViewLocator.Generation | Error | Mapped view cannot be constructed
SVL0005 | StaticViewLocator.Generation | Error | Locator type cannot be extended by generated source
SVL0006 | StaticViewLocator.Generation | Error | Configured view-model mapping contract is invalid
9 changes: 9 additions & 0 deletions StaticViewLocator/StaticViewLocatorGenerator.Adapters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ public sealed partial class StaticViewLocatorGenerator
isEnabledByDefault: true,
description: "Generated partial declarations cannot extend static or file-local locator classes.");

private static readonly DiagnosticDescriptor InvalidMappingContract = new(
id: "SVL0006",
title: "Configured view-model mapping contract is invalid",
messageFormat: "Mapping contract '{0}' must be an open generic interface or class with exactly one type parameter",
category: "StaticViewLocator.Generation",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Configured contract discovery requires an open generic interface or class with exactly one type parameter.");

private static Dictionary<INamedTypeSymbol, INamedTypeSymbol> GetReactiveUIMappings(
Compilation compilation,
HashSet<INamedTypeSymbol> viewBaseTypes,
Expand Down
21 changes: 18 additions & 3 deletions StaticViewLocator/StaticViewLocatorGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1194,7 +1194,7 @@ private static Dictionary<INamedTypeSymbol, INamedTypeSymbol> GetInferredMapping
ICollection<Diagnostic> diagnostics,
HashSet<INamedTypeSymbol> ambiguousViewModels)
{
var contracts = GetViewModelMappingContracts(locatorSymbol);
var contracts = GetViewModelMappingContracts(locatorSymbol, diagnostics);
var mappings = new Dictionary<INamedTypeSymbol, INamedTypeSymbol>(SymbolEqualityComparer.Default);
if (contracts.Count == 0)
{
Expand Down Expand Up @@ -1247,7 +1247,9 @@ private static Dictionary<INamedTypeSymbol, INamedTypeSymbol> GetInferredMapping
return mappings;
}

private static HashSet<INamedTypeSymbol> GetViewModelMappingContracts(INamedTypeSymbol locatorSymbol)
private static HashSet<INamedTypeSymbol> GetViewModelMappingContracts(
INamedTypeSymbol locatorSymbol,
ICollection<Diagnostic> diagnostics)
{
var contracts = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
var locatorAttribute = locatorSymbol.GetAttributes()
Expand All @@ -1266,10 +1268,23 @@ private static HashSet<INamedTypeSymbol> GetViewModelMappingContracts(INamedType

foreach (var value in argument.Value.Values)
{
if (value.Value is INamedTypeSymbol contract && contract.Arity == 1)
if (value.Value is not INamedTypeSymbol contract)
{
continue;
}

if (contract.Arity == 1 &&
(contract.TypeKind == TypeKind.Interface || contract.TypeKind == TypeKind.Class) &&
contract.IsUnboundGenericType)
{
contracts.Add(contract.OriginalDefinition);
continue;
}

diagnostics.Add(Diagnostic.Create(
InvalidMappingContract,
GetDiagnosticLocation(locatorSymbol),
contract.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
}
}

Expand Down
Loading