diff --git a/Schema.Test/CodeGenerationTests.cs b/Schema.Test/CodeGenerationTests.cs index 57715b1..a991a6f 100644 --- a/Schema.Test/CodeGenerationTests.cs +++ b/Schema.Test/CodeGenerationTests.cs @@ -248,17 +248,17 @@ public void TestNoNamespaceIsEmittedWhenNotConfigured() [DataRow("Ratio", "public float Ratio")] [DataRow("Precise", "public double Precise")] [DataRow("Name", "public string Name")] - [DataRow("CreatedAt", "public System.DateTime CreatedAt")] - [DataRow("Uptime", "public System.TimeSpan Uptime")] - [DataRow("Position2", "public System.Numerics.Vector2 Position2")] - [DataRow("Position3", "public System.Numerics.Vector3 Position3")] - [DataRow("Position4", "public System.Numerics.Vector4 Position4")] - [DataRow("Tint", "public ktsu.Schema.Runtime.ColorRgb Tint")] - [DataRow("Overlay", "public ktsu.Schema.Runtime.ColorRgba Overlay")] + [DataRow("CreatedAt", "public global::System.DateTime CreatedAt")] + [DataRow("Uptime", "public global::System.TimeSpan Uptime")] + [DataRow("Position2", "public global::System.Numerics.Vector2 Position2")] + [DataRow("Position3", "public global::System.Numerics.Vector3 Position3")] + [DataRow("Position4", "public global::System.Numerics.Vector4 Position4")] + [DataRow("Tint", "public global::ktsu.Schema.Runtime.ColorRgb Tint")] + [DataRow("Overlay", "public global::ktsu.Schema.Runtime.ColorRgba Overlay")] [DataRow("Role", "public Role Role")] [DataRow("Favourite", "public Item Favourite")] - [DataRow("Tags", "public System.Collections.Generic.List Tags")] - [DataRow("Items", "public System.Collections.Generic.Dictionary Items")] + [DataRow("Tags", "public global::System.Collections.Generic.List Tags")] + [DataRow("Items", "public global::System.Collections.Generic.Dictionary Items")] public void TestEveryTypeMapsAsDocumented(string memberName, string expectedDeclaration) { Schema schema = CreateFullSchema(); diff --git a/Schema.Test/GeneratedNameCollisionTests.cs b/Schema.Test/GeneratedNameCollisionTests.cs new file mode 100644 index 0000000..1f9e2ef --- /dev/null +++ b/Schema.Test/GeneratedNameCollisionTests.cs @@ -0,0 +1,131 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Schema.Tests; + +using System.Collections.ObjectModel; + +using ktsu.Schema.Generation; +using ktsu.Schema.Models; +using ktsu.Schema.Models.Names; +using ktsu.Schema.Models.Types; +using ktsu.Semantics.Strings; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Names a schema may legitimately choose that collide with something the generated C# needs. +/// +/// +/// Two kinds. A class or namespace named System or ktsu shadows the namespace a +/// generated type reference starts with, which every reference now avoids by being written from +/// global::. A member named after the type declaring it has no spelling at all in C# or +/// C++, so validation refuses it before a generator is asked. +/// +[TestClass] +public sealed class GeneratedNameCollisionTests +{ + /// + /// A class called System beside a class whose members are spelled with System + /// types. Without global:: the second class resolved System.Numerics against the + /// first and did not compile. + /// + [TestMethod] + [DataRow("System")] + [DataRow("ktsu")] + public void AClassNamedAfterARootNamespaceStillCompiles(string name) + { + Schema schema = new(); + schema.AddClass(name.As())!.AddMember("Id".As())!.SetType(new Int()); + AddTransform(schema); + + CompileGenerated(schema, "Generated"); + } + + /// + /// A generator namespace whose last segment is System shadows the root namespace for + /// every type declared inside it. + /// + [TestMethod] + public void ANamespaceEndingInSystemStillCompiles() + { + Schema schema = new(); + AddTransform(schema); + + CompileGenerated(schema, "Game.System"); + } + + [TestMethod] + public void AMemberNamedAfterItsClassIsRefused() + { + Schema schema = new(); + schema.AddClass("Score".As())!.AddMember("Score".As())!.SetType(new Int()); + + AssertRefused(schema, "Score.Score", "Member 'Score' has the same name as its class"); + } + + [TestMethod] + public void ASemanticTypeNamedValueIsRefused() + { + Schema schema = new(); + schema.AddSemanticType("Value".As())!.SetUnderlyingType(new Float()); + + AssertRefused(schema, "Value", "Semantic type 'Value' has the same name as the 'Value' member"); + } + + [TestMethod] + public void AFunctionNamedAfterItsInterfaceIsRefused() + { + Schema schema = new(); + schema.AddInterface("Draw".As())! + .AddFunction("Draw".As())! + .SetReturnType(new Void()); + + AssertRefused(schema, "Draw.Draw", "Function 'Draw' has the same name as its interface"); + } + + /// + /// The comparison is ordinal, because both languages are case sensitive: score in + /// Score is a legal declaration and stays accepted. + /// + [TestMethod] + public void ANameDifferingOnlyInCaseIsAccepted() + { + Schema schema = new(); + schema.AddClass("Score".As())!.AddMember("score".As())!.SetType(new Int()); + + Collection issues = schema.Validate(); + + Assert.IsEmpty(issues, string.Join("; ", issues)); + } + + private static void AddTransform(Schema schema) + { + SchemaClass transform = schema.AddClass("Transform".As())!; + transform.AddMember("Position".As())!.SetType(new Vector3()); + transform.AddMember("Tags".As())!.SetType(new Array + { + ElementType = new String(), + Container = Array.VectorContainer.As(), + }); + transform.AddMember("Created".As())!.SetType(new DateTime()); + } + + private static void CompileGenerated(Schema schema, string codeNamespace) + { + SchemaGenerationResult result = SchemaGenerator.Generate(schema, CodeGenerationTests.ConfigureGenerator(schema, codeNamespace)); + Assert.IsTrue(result.IsSuccess, result.Message); + GeneratedSourceCompiler.Compile(result.Files); + } + + private static void AssertRefused(Schema schema, string path, string message) + { + Collection issues = schema.Validate(); + + Assert.Contains( + i => i.Severity == SchemaValidationSeverity.Error + && i.Path == path + && i.Message.Contains(message, System.StringComparison.Ordinal), + issues, + string.Join("; ", issues)); + } +} diff --git a/Schema.Test/GeneratorRegistryTests.cs b/Schema.Test/GeneratorRegistryTests.cs index debd060..5886078 100644 --- a/Schema.Test/GeneratorRegistryTests.cs +++ b/Schema.Test/GeneratorRegistryTests.cs @@ -20,10 +20,15 @@ namespace ktsu.Schema.Tests; /// found by the language a schema names, which is the whole reason the registry is not a fixed /// table. /// -/// The registry is process-wide, so each test here registers a language nothing else uses. +/// The registry is process-wide, so each test here registers a language nothing else uses. It is +/// also an unsynchronised dictionary, documented as meant to be filled once at start-up, so two of +/// these tests registering at once can lose a write while it resizes. That failed CI once on +/// Windows, with a generator registered a line earlier reported as unknown, so the class runs its +/// tests one at a time. /// /// [TestClass] +[DoNotParallelize] public sealed class GeneratorRegistryTests { /// diff --git a/Schema/Cli/SchemaCommandLine.cs b/Schema/Cli/SchemaCommandLine.cs index 1d9bb90..d240490 100644 --- a/Schema/Cli/SchemaCommandLine.cs +++ b/Schema/Cli/SchemaCommandLine.cs @@ -66,7 +66,7 @@ public static int Run(string[] args, TextWriter output, TextWriter error, Schema { "generate" => RejectUnknownOptions([.. args.Skip(1)], [GeneratorOption], output, error, host) ?? Generate([.. args.Skip(1)], output, error), - "validate" => RejectUnknownOptions([.. args.Skip(1)], [], output, error, host) + "validate" => RejectUnknownOptions([.. args.Skip(1)], [NoDataOption], output, error, host) ?? Validate([.. args.Skip(1)], output, error), _ => UnknownCommand(args[0], output, error, host), }; diff --git a/Schema/Generation/CSharpCodeGenerator.cs b/Schema/Generation/CSharpCodeGenerator.cs index f75f2ba..d8ff0da 100644 --- a/Schema/Generation/CSharpCodeGenerator.cs +++ b/Schema/Generation/CSharpCodeGenerator.cs @@ -110,8 +110,8 @@ private static string GenerateDataRecord(SchemaClass schemaClass, CodeNamespace string name = RecordTypeName(schemaClass); WriteSummary(code, $"One {Escape(schemaClass.Name)} in a data file, read and written in place."); - code.WriteLine($"public sealed class {name}(System.Text.Json.Nodes.JsonObject json, System.Action onEdited)"); - code.WriteLine("\t: ktsu.Schema.Runtime.SchemaDataRecord(json, onEdited)"); + code.WriteLine($"public sealed class {name}(global::System.Text.Json.Nodes.JsonObject json, global::System.Action onEdited)"); + code.WriteLine("\t: global::ktsu.Schema.Runtime.SchemaDataRecord(json, onEdited)"); using (new Scope(code)) { bool first = true; @@ -136,11 +136,11 @@ private static string GenerateDataRecord(SchemaClass schemaClass, CodeNamespace code.WriteLine($"public {type} {CSharpKeywords.Identifier(member.Name)}"); using (new Scope(code)) { - code.WriteLine($"get => ktsu.Schema.Runtime.SchemaDataValues.Get{accessor}{typeArguments}(Json, {key});"); + code.WriteLine($"get => global::ktsu.Schema.Runtime.SchemaDataValues.Get{accessor}{typeArguments}(Json, {key});"); code.WriteLine("set"); using (new Scope(code)) { - code.WriteLine($"ktsu.Schema.Runtime.SchemaDataValues.Set{accessor}{typeArguments}(Json, {key}, value);"); + code.WriteLine($"global::ktsu.Schema.Runtime.SchemaDataValues.Set{accessor}{typeArguments}(Json, {key}, value);"); code.WriteLine("MarkEdited();"); } } @@ -162,15 +162,15 @@ private static string GenerateDataRecord(SchemaClass schemaClass, CodeNamespace Float => ("float", "Float", null), Double => ("double", "Double", null), Models.Types.String => ("string", "String", null), - Models.Types.DateTime => ("System.DateTime", "DateTime", null), - Models.Types.TimeSpan => ("System.TimeSpan", "TimeSpan", null), + Models.Types.DateTime => ("global::System.DateTime", "DateTime", null), + Models.Types.TimeSpan => ("global::System.TimeSpan", "TimeSpan", null), // The colours before their vector bases, as in MapType. - ColorRGB => ("ktsu.Schema.Runtime.ColorRgb", "ColorRgb", null), - ColorRGBA => ("ktsu.Schema.Runtime.ColorRgba", "ColorRgba", null), - Vector2 { ElementType: Float } => ("System.Numerics.Vector2", "Vector2", null), - Vector3 { ElementType: Float } => ("System.Numerics.Vector3", "Vector3", null), - Vector4 { ElementType: Float } => ("System.Numerics.Vector4", "Vector4", null), + ColorRGB => ("global::ktsu.Schema.Runtime.ColorRgb", "ColorRgb", null), + ColorRGBA => ("global::ktsu.Schema.Runtime.ColorRgba", "ColorRgba", null), + Vector2 { ElementType: Float } => ("global::System.Numerics.Vector2", "Vector2", null), + Vector3 { ElementType: Float } => ("global::System.Numerics.Vector3", "Vector3", null), + Vector4 { ElementType: Float } => ("global::System.Numerics.Vector4", "Vector4", null), Models.Types.Enum enumType => (CSharpKeywords.Identifier(enumType.EnumName), "Enum", CSharpKeywords.Identifier(enumType.EnumName)), _ => null, }; @@ -197,7 +197,7 @@ private static string GenerateDataEditor(Models.Schema schema, DataSource dataSo WriteSummary(code, $"Loads, validates, edits and saves the {Escape(dataSource.Name)} data file, whose records are each a {Escape(boundClass.Name)}."); } - code.WriteLine($"public sealed class {name} : ktsu.Schema.Runtime.SchemaDataEditor<{record}>"); + code.WriteLine($"public sealed class {name} : global::ktsu.Schema.Runtime.SchemaDataEditor<{record}>"); using (new Scope(code)) { WriteSummary(code, "The schema this editor was generated from, as its file holds it."); @@ -284,8 +284,8 @@ private static string GenerateClass(SchemaClass schemaClass, CodeNamespace codeN if (schemaClass.TravelsAsBytes) { - code.WriteLine("[ktsu.Schema.Runtime.SchemaTravelsAsBytes]"); - code.WriteLine("[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]"); + code.WriteLine("[global::ktsu.Schema.Runtime.SchemaTravelsAsBytes]"); + code.WriteLine("[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential)]"); } code.WriteLine($"public {(schemaClass.TravelsAsBytes ? "struct" : "class")} {CSharpKeywords.Identifier(schemaClass.Name)}"); @@ -344,7 +344,7 @@ private static void WriteMarshalAttribute(CodeBlocker code, BaseType type, bool // `field:` because the member is an auto-property and the attribute is only legal on a // field. That is also the declaration it has to reach: the backing field is what the // marshaller lays out, and what the property is only a pair of accessors over. - code.WriteLine("[field: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.U1)]"); + code.WriteLine("[field: global::System.Runtime.InteropServices.MarshalAs(global::System.Runtime.InteropServices.UnmanagedType.U1)]"); } } @@ -398,9 +398,9 @@ private static string GenerateSemanticType(SchemaSemanticType semanticType, Code ? string.Empty : $"(Refines = typeof({CSharpKeywords.Identifier(refined.Name)}))"; - code.WriteLine($"[ktsu.Schema.Runtime.SchemaSemanticType{refines}]"); + code.WriteLine($"[global::ktsu.Schema.Runtime.SchemaSemanticType{refines}]"); WriteMetadataAttributes(code, semanticType); - code.WriteLine("[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]"); + code.WriteLine("[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential)]"); code.WriteLine($"public readonly record struct {name}"); using (new Scope(code)) @@ -511,7 +511,7 @@ private static void WriteFunction(CodeBlocker code, SchemaFunction function) if (function.IsQuery) { - code.WriteLine("[ktsu.Schema.Runtime.SchemaQuery]"); + code.WriteLine("[global::ktsu.Schema.Runtime.SchemaQuery]"); } IEnumerable parameters = function.Parameters.Select(MapParameter); @@ -557,8 +557,8 @@ private static string MapParameter(SchemaParameter parameter) { string element = MapType(span.ElementType); string view = parameter.Direction == ParameterDirection.In - ? $"System.ReadOnlySpan<{element}>" - : $"System.Span<{element}>"; + ? $"global::System.ReadOnlySpan<{element}>" + : $"global::System.Span<{element}>"; return $"{view} {name}"; } @@ -671,7 +671,7 @@ private static void WriteSchemaKeyAttribute(CodeBlocker code, BaseType type) string.Equals(arrayType.Container, ContainerName.MapName, StringComparison.Ordinal) && !string.IsNullOrEmpty(arrayType.Key)) { - code.WriteLine($"[ktsu.Schema.Runtime.SchemaKey(\"{arrayType.Key}\")]"); + code.WriteLine($"[global::ktsu.Schema.Runtime.SchemaKey(\"{arrayType.Key}\")]"); } } @@ -693,33 +693,33 @@ private static void WriteMetadataAttributes(CodeBlocker code, ISchemaMetadataCar { if (carrier.Unit is not null) { - code.WriteLine($"[ktsu.Schema.Runtime.SchemaUnit({Quote(carrier.Unit)})]"); + code.WriteLine($"[global::ktsu.Schema.Runtime.SchemaUnit({Quote(carrier.Unit)})]"); } if (carrier.Range is MemberRange range) { string wrap = range.Wrap ? ", Wrap = true" : string.Empty; - code.WriteLine($"[ktsu.Schema.Runtime.SchemaRange({Literal(range.Minimum)}, {Literal(range.Maximum)}{wrap})]"); + code.WriteLine($"[global::ktsu.Schema.Runtime.SchemaRange({Literal(range.Minimum)}, {Literal(range.Maximum)}{wrap})]"); } if (DefaultArgument(carrier.DefaultValue) is string argument) { - code.WriteLine($"[ktsu.Schema.Runtime.SchemaDefault({argument})]"); + code.WriteLine($"[global::ktsu.Schema.Runtime.SchemaDefault({argument})]"); } if (carrier.Interpolation is not Interpolation.None) { - code.WriteLine($"[ktsu.Schema.Runtime.SchemaInterpolation(ktsu.Schema.Models.Metadata.Interpolation.{carrier.Interpolation})]"); + code.WriteLine($"[global::ktsu.Schema.Runtime.SchemaInterpolation(global::ktsu.Schema.Models.Metadata.Interpolation.{carrier.Interpolation})]"); } if (carrier.Network is MemberNetwork network) { - code.WriteLine($"[ktsu.Schema.Runtime.SchemaNetwork({Literal(network.Quantise)}, {(network.Delta ? "true" : "false")})]"); + code.WriteLine($"[global::ktsu.Schema.Runtime.SchemaNetwork({Literal(network.Quantise)}, {(network.Delta ? "true" : "false")})]"); } if (carrier.Editor is not null) { - code.WriteLine($"[ktsu.Schema.Runtime.SchemaEditorHint({Quote(carrier.Editor)})]"); + code.WriteLine($"[global::ktsu.Schema.Runtime.SchemaEditorHint({Quote(carrier.Editor)})]"); } } @@ -806,27 +806,27 @@ private static string Escape(string text) => Float => "float", Double => "double", Models.Types.String => "string", - Models.Types.DateTime => "System.DateTime", - Models.Types.TimeSpan => "System.TimeSpan", + Models.Types.DateTime => "global::System.DateTime", + Models.Types.TimeSpan => "global::System.TimeSpan", // The colours before their vector bases: ColorRGB derives from Vector3. - ColorRGB => "ktsu.Schema.Runtime.ColorRgb", - ColorRGBA => "ktsu.Schema.Runtime.ColorRgba", + ColorRGB => "global::ktsu.Schema.Runtime.ColorRgb", + ColorRGBA => "global::ktsu.Schema.Runtime.ColorRgba", // System.Numerics vectors hold floats and nothing else, so they represent a vector of // floats and only that. A vector of anything else is one of this library's, which is // generic over the component precisely because the System.Numerics ones are not. - Vector2 { ElementType: Float } => "System.Numerics.Vector2", - Vector3 { ElementType: Float } => "System.Numerics.Vector3", - Vector4 { ElementType: Float } => "System.Numerics.Vector4", - Vector2 vector => $"ktsu.Schema.Runtime.Vector2<{MapType(vector.ElementType)}>", - Vector3 vector => $"ktsu.Schema.Runtime.Vector3<{MapType(vector.ElementType)}>", - Vector4 vector => $"ktsu.Schema.Runtime.Vector4<{MapType(vector.ElementType)}>", + Vector2 { ElementType: Float } => "global::System.Numerics.Vector2", + Vector3 { ElementType: Float } => "global::System.Numerics.Vector3", + Vector4 { ElementType: Float } => "global::System.Numerics.Vector4", + Vector2 vector => $"global::ktsu.Schema.Runtime.Vector2<{MapType(vector.ElementType)}>", + Vector3 vector => $"global::ktsu.Schema.Runtime.Vector3<{MapType(vector.ElementType)}>", + Vector4 vector => $"global::ktsu.Schema.Runtime.Vector4<{MapType(vector.ElementType)}>", // What the handle names is a type argument rather than anything stored, so this says which // handles may be passed where without saying that a handle to a mesh is laid out // differently from a handle to a texture - it is not. - Handle handle => $"ktsu.Schema.Runtime.Handle<{MapType(handle.ElementType)}>", + Handle handle => $"global::ktsu.Schema.Runtime.Handle<{MapType(handle.ElementType)}>", // The struct emitted for the semantic type, named the same way a class or an enum is. Semantic semanticType => CSharpKeywords.Identifier(semanticType.SemanticTypeName), @@ -835,14 +835,14 @@ private static string Escape(string text) => // A quantity is written where it lives, closed over the number it is stored in, and // nothing is emitted for it - which is the whole of what makes it different from the // semantic type above. - Quantity quantity => $"ktsu.Semantics.Quantities.{quantity.QuantityName}<{MapType(quantity.Storage)}>", + Quantity quantity => $"global::ktsu.Semantics.Quantities.{quantity.QuantityName}<{MapType(quantity.Storage)}>", Models.Types.Enum enumType => CSharpKeywords.Identifier(enumType.EnumName), Models.Types.Object objectType => CSharpKeywords.Identifier(objectType.ClassName), Models.Types.Interface interfaceType => CSharpKeywords.Identifier(interfaceType.InterfaceName), Models.Types.Array arrayType => MapArray(arrayType), - Optional optional => $"ktsu.Schema.Runtime.Optional<{MapType(optional.ElementType)}>", + Optional optional => $"global::ktsu.Schema.Runtime.Optional<{MapType(optional.ElementType)}>", Models.Types.Result result => MapResult(result), // A member left as None is reported by validation as a warning, not an error, so @@ -878,8 +878,8 @@ private static string MapResult(Models.Types.Result result) string failure = CSharpKeywords.Identifier(error); return result.ElementType is Models.Types.Void - ? $"ktsu.Schema.Runtime.Result<{failure}>" - : $"ktsu.Schema.Runtime.Result<{MapType(result.ElementType)}, {failure}>"; + ? $"global::ktsu.Schema.Runtime.Result<{failure}>" + : $"global::ktsu.Schema.Runtime.Result<{MapType(result.ElementType)}, {failure}>"; } private static string MapArray(Models.Types.Array arrayType) @@ -889,10 +889,10 @@ private static string MapArray(Models.Types.Array arrayType) if (!string.Equals(arrayType.Container, ContainerName.MapName, StringComparison.Ordinal)) { // vector, and any container a consumer has invented, is a sequence. - return $"System.Collections.Generic.List<{elementType}>"; + return $"global::System.Collections.Generic.List<{elementType}>"; } - return $"System.Collections.Generic.Dictionary<{MapKeyType(arrayType)}, {elementType}>"; + return $"global::System.Collections.Generic.Dictionary<{MapKeyType(arrayType)}, {elementType}>"; } /// diff --git a/Schema/Models/Schema.Validation.cs b/Schema/Models/Schema.Validation.cs index a2b2895..6284691 100644 --- a/Schema/Models/Schema.Validation.cs +++ b/Schema/Models/Schema.Validation.cs @@ -175,6 +175,7 @@ private void ValidateClasses(Collection issues) { string memberPath = $"{classPath}.{PathSegment(member.Name)}"; ValidateNameNotEmpty(issues, member.Name, "Member", memberPath, member); + ValidateNotNamedAfterItsType(issues, member.Name, schemaClass.Name, $"Member '{member.Name}' has the same name as its class", memberPath, member); if (member.Type is None) { @@ -197,6 +198,30 @@ private void ValidateClasses(Collection issues) ValidateContainmentCycles(issues); } + /// + /// The member a generated semantic type keeps its value in, which is a name the schema does + /// not see but the type's own name must not take. + /// + private const string SemanticValueMember = "Value"; + + /// + /// Reports a member that would be declared with the name of the type declaring it. + /// + /// + /// C# refuses it outright (CS0542), and so does C++ for a member function or a data member of + /// a class with a constructor, and neither language has an escape for it - a verbatim + /// @Label is still the name Label. So the schema is refused here, naming both, + /// rather than validating cleanly and failing when the consumer builds the generated code. + /// The comparison is ordinal: label in a class Label is a different name. + /// + private static void ValidateNotNamedAfterItsType(Collection issues, string memberName, string typeName, string what, string path, ISchemaElement element) + { + if (!string.IsNullOrEmpty(memberName) && string.Equals(memberName, typeName, StringComparison.Ordinal)) + { + Report(issues, path, element, $"{what}; a member cannot be named after the type that declares it. Rename one of them."); + } + } + /// /// Reports every class that holds itself by value, directly or through other classes. /// @@ -743,6 +768,7 @@ private void ValidateSemanticTypes(Collection issues) { string path = PathSegment(semanticType.Name); ValidateNameNotEmpty(issues, semanticType.Name, "Semantic type", path, semanticType); + ValidateNotNamedAfterItsType(issues, SemanticValueMember, semanticType.Name, $"Semantic type '{semanticType.Name}' has the same name as the '{SemanticValueMember}' member that holds its value", path, semanticType); ValidateUnderlyingType(issues, semanticType, path); ValidateMetadata(issues, semanticType, semanticType.Representation(), path, semanticType, "semantic type"); } @@ -826,7 +852,9 @@ private void ValidateInterfaces(Collection issues) foreach (SchemaFunction function in schemaInterface.Functions) { - ValidateFunction(issues, function, $"{interfacePath}.{PathSegment(function.Name)}"); + string functionPath = $"{interfacePath}.{PathSegment(function.Name)}"; + ValidateNotNamedAfterItsType(issues, function.Name, schemaInterface.Name, $"Function '{function.Name}' has the same name as its interface", functionPath, function); + ValidateFunction(issues, function, functionPath); } } }