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
45 changes: 45 additions & 0 deletions Schema.Test/SchemaSemanticTypeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,51 @@ public void AnUnresolvedRefinementIsRejected()
Assert.Contains(i => i.Message.Contains("does not declare", StringComparison.Ordinal), schema.Validate());
}

/// <summary>
/// A chain of refinement ending at a name the schema does not declare is that one unresolved
/// name, and nothing else.
/// </summary>
/// <remarks>
/// Walking the chain stops both at a type already seen and at a link naming nothing, so the two
/// used to be indistinguishable to whoever asked: <c>A</c> was told it refined itself through a
/// cycle that does not exist, and the declaration to fix - <c>B</c>, which names the missing
/// type - was the one the message did not name. Depth one never reaches that branch, which is
/// why the case above did not catch it.
/// </remarks>
[TestMethod]
public void AnUnresolvedRefinementDeeperInAChainIsNotACycle()
{
Schema schema = new();
SchemaSemanticType weight = schema.AddSemanticType("Weight".As<SemanticTypeName>())!;
SchemaSemanticType force = schema.AddSemanticType("ForceMagnitude".As<SemanticTypeName>())!;
weight.SetUnderlyingType(new Semantic { SemanticTypeName = "ForceMagnitude".As<SemanticTypeName>() });
force.SetUnderlyingType(new Semantic { SemanticTypeName = "Missing".As<SemanticTypeName>() });

SchemaValidationIssue issue = schema.Validate().Single();

Assert.Contains("does not declare", issue.Message, StringComparison.Ordinal);
Assert.AreSame(force, issue.Element);
Assert.IsFalse(weight.RefinesItself());
}

/// <summary>
/// A cycle is still a cycle when the type that closes it is not the one asked.
/// </summary>
[TestMethod]
public void ARefinementChainThatRevisitsATypeStillReportsItself()
{
Schema schema = new();
SchemaSemanticType a = schema.AddSemanticType("A".As<SemanticTypeName>())!;
SchemaSemanticType b = schema.AddSemanticType("B".As<SemanticTypeName>())!;
SchemaSemanticType c = schema.AddSemanticType("C".As<SemanticTypeName>())!;
a.SetUnderlyingType(new Semantic { SemanticTypeName = "B".As<SemanticTypeName>() });
b.SetUnderlyingType(new Semantic { SemanticTypeName = "C".As<SemanticTypeName>() });
c.SetUnderlyingType(new Semantic { SemanticTypeName = "B".As<SemanticTypeName>() });

Assert.IsTrue(a.RefinesItself());
Assert.Contains(i => i.Message.Contains("refines itself", StringComparison.Ordinal), schema.Validate());
}

/// <summary>
/// A semantic type with no underlying type chosen is not generatable.
/// </summary>
Expand Down
9 changes: 6 additions & 3 deletions Schema/Models/Schema.Validation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@
/// <summary>
/// A default has to be of the member's own kind, and inside its range.
/// </summary>
private static void ValidateMemberDefault(Collection<SchemaValidationIssue> issues, ISchemaMetadataCarrier carrier, BaseType type, string path, ISchemaElement element, string kind)

Check warning on line 343 in Schema/Models/Schema.Validation.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 343 in Schema/Models/Schema.Validation.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 343 in Schema/Models/Schema.Validation.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 343 in Schema/Models/Schema.Validation.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 343 in Schema/Models/Schema.Validation.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 343 in Schema/Models/Schema.Validation.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 343 in Schema/Models/Schema.Validation.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 343 in Schema/Models/Schema.Validation.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 343 in Schema/Models/Schema.Validation.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 343 in Schema/Models/Schema.Validation.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.
{
switch (carrier.DefaultValue)
{
Expand Down Expand Up @@ -585,9 +585,12 @@
return;
}

// Refines() stops at the first type it has already seen, so a cycle shows up as a chain
// that never reaches a non-semantic type rather than as a hang.
if (!semanticType.Refines().Any(refined => refined.UnderlyingType is not Semantic))
// Asked whether the chain reached anything real, a chain ending at a name the schema does
// not declare answers the same as a cycle, and the author is sent looking for one that is
// not there. RefinesItself() asks the question this message makes: whether the chain came
// back to a type it had already seen. The unresolved tail is reported at the declaration
// naming it, which is the declaration to fix.
if (semanticType.RefinesItself())
{
Report(issues, path, semanticType, $"Semantic type '{semanticType.Name}' refines itself, directly or through a cycle, so it is represented as nothing.");
}
Expand Down
21 changes: 21 additions & 0 deletions Schema/Models/SchemaSemanticType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,27 @@ public IEnumerable<SchemaSemanticType> Refines()
}
}

/// <summary>
/// Gets whether the chain of refinement from this type comes back to a type already seen.
/// </summary>
/// <remarks>
/// <see cref="Refines()"/> stops for two reasons, and the chain looks the same from the outside
/// either way: a type it has already seen, which is a cycle, and a link naming something this
/// schema does not declare, which is not. Only the first is this type's to answer for — an
/// unresolved name is reported at the declaration that names it, which is where the fix is — so
/// telling the two apart is what stops a typo being reported as a cycle that does not exist.
/// <para>
/// The chain ended on a cycle exactly when the deepest type it reached still refines something
/// that resolves, since that is the link <see cref="Refines()"/> refused to follow a second time.
/// </para>
/// </remarks>
/// <returns><see langword="true"/> when the chain revisits a type.</returns>
public bool RefinesItself()
{
SchemaSemanticType deepest = Refines().LastOrDefault() ?? this;
return deepest.UnderlyingType is Semantic semantic && semantic.Declaration is not null;
}

/// <summary>
/// Gets the type values of this one are ultimately stored as, following any chain of
/// refinement down to the first type that is not itself semantic.
Expand Down