diff options
| author | Karsten Heimrich <karsten.heimrich@qt.io> | 2025-10-31 22:35:46 +0100 |
|---|---|---|
| committer | Miguel Costa <miguel.costa@qt.io> | 2025-11-10 17:50:42 +0000 |
| commit | 23334513211ad17cee183825e9d20967ff868b90 (patch) | |
| tree | 51704b6c8ac8d2763b2999b4110c6d1e06645221 | |
| parent | fb33a6c15480505a953d7fcb33ad100914c15eea (diff) | |
Validate QmlElement.Name and emit correct macros or error
- GenerateClass:
Precompute QML macros. If Name is explicitly provided but invalid,
return a error (descriptive) instead of silently emitting QML_ELEMENT.
Otherwise:
* QML_ELEMENT when Name is omitted
* QML_NAMED_ELEMENT(<Name>) when valid
- TypeExtensionsForGenerationRules:
Validate using ^[A-Z][A-Za-z0-9_]*$(First char uppercase, followed by
letters/digits/underscores; rejects whitespace/special chars).
- TestCodeGenerator:
Include rule error messages in the thrown exception to aid debugging.
- Tests:
Verify named vs unnamed element macro emission, invalid-name failures,
and singleton macro presence.
Change-Id: I054728c625a7fff6b1dfcbf512811672ce18f38e
Reviewed-by: Miguel Costa <miguel.costa@qt.io>
4 files changed, 150 insertions, 8 deletions
diff --git a/src/Qt.DotNet.GenerationRules/Qt/DotNet/CodeGeneration/Rules/Class/GenerateClass.cs b/src/Qt.DotNet.GenerationRules/Qt/DotNet/CodeGeneration/Rules/Class/GenerateClass.cs index a0e8983..8d064d6 100644 --- a/src/Qt.DotNet.GenerationRules/Qt/DotNet/CodeGeneration/Rules/Class/GenerateClass.cs +++ b/src/Qt.DotNet.GenerationRules/Qt/DotNet/CodeGeneration/Rules/Class/GenerateClass.cs @@ -10,6 +10,7 @@ namespace Qt.DotNet.CodeGeneration.Rules.Class { using MetaFunctions; using Extensions; + using Quick; using static Placeholders; using static Traits; @@ -23,7 +24,8 @@ namespace Qt.DotNet.CodeGeneration.Rules.Class //////////////////////////////////////////////////////////////////////////////////////// // - if (type.IsQmlElement()) { + var isQmlElement = type.IsQmlElement(); + if (isQmlElement) { if (Root.GetPlaceholder(IncludeDirs) is not { } includeDirs) return Error(); includeDirs += $"include_directories({Hpp}/{type.MFn(Ns | Dir)})"; @@ -34,6 +36,25 @@ namespace Qt.DotNet.CodeGeneration.Rules.Class } //////////////////////////////////////////////////////////////////////////////////////// + // Precompute QML macros and validate provided Name (fail fast on invalid) + var elementName = isQmlElement ? type.QmlElementName() : null; + var nameProvided = isQmlElement && type.QtAttributeData<QmlElementAttribute>() + .Any(a => a.HasProperty(nameof(QmlElementAttribute.Name))); + + if (nameProvided && string.IsNullOrWhiteSpace(elementName)) { + return Error($"QmlElement.Name on '{type.MFn(Src | Fqn)}' is invalid. It must " + + "start with an uppercase letter and contain only letters, digits, or '_'."); + } + + var qmlElementMacro = Wrap.ToString(); + if (isQmlElement) { + qmlElementMacro = !string.IsNullOrEmpty(elementName) + ? $"QML_NAMED_ELEMENT({elementName})" + : "QML_ELEMENT"; + } + var qmlSingletonMacro = type.IsQmlSingleton() ? "QML_SINGLETON" : Wrap.ToString(); + + //////////////////////////////////////////////////////////////////////////////////////// // if (type.GetPlaceholder(ForwardDecl) is not { } forwardDecl) return Error(); @@ -79,10 +100,8 @@ class {type.MFn(Ns | Name)} : public QDotNetObject {{ Q_OBJECT - {(!type.IsQmlElement() ? Wrap : type.QmlElementName() is not { Length: > 0 } elementName - ? "QML_ELEMENT" - : $"QML_NAMED_ELEMENT({elementName})")} - {(type.IsQmlSingleton() ? "QML_SINGLETON" : Wrap)} + {qmlElementMacro} + {qmlSingletonMacro} {publicDecl[(Placeholder)new(TypeTraits)]} public: Q_DOTNET_OBJECT({type.MFn(Name)}, diff --git a/src/Qt.DotNet.GenerationRules/Qt/DotNet/Extensions/TypeExtensionsForGenerationRules.cs b/src/Qt.DotNet.GenerationRules/Qt/DotNet/Extensions/TypeExtensionsForGenerationRules.cs index 57a47ae..2bc851e 100644 --- a/src/Qt.DotNet.GenerationRules/Qt/DotNet/Extensions/TypeExtensionsForGenerationRules.cs +++ b/src/Qt.DotNet.GenerationRules/Qt/DotNet/Extensions/TypeExtensionsForGenerationRules.cs @@ -114,7 +114,7 @@ namespace Qt.DotNet.Extensions .Any(a => a.TryProperty(nameof(QmlElementAttribute.Singleton), out bool ok) && ok); } - private static readonly Regex QmlRegex = new(@"^(?!\d)\w+$", RegexOptions.Compiled); + private static readonly Regex QmlRegex = new("^[A-Z][A-Za-z0-9_]*$", RegexOptions.Compiled); public static string QmlElementName(this Type self) { diff --git a/tests/Test_Qt.DotNet.Generator/Support/TestCodeGenerator.cs b/tests/Test_Qt.DotNet.Generator/Support/TestCodeGenerator.cs index fab062e..8d46c18 100644 --- a/tests/Test_Qt.DotNet.Generator/Support/TestCodeGenerator.cs +++ b/tests/Test_Qt.DotNet.Generator/Support/TestCodeGenerator.cs @@ -145,8 +145,12 @@ namespace Test_Qt.DotNet.Generator.Support Directory.CreateDirectory(targetDirectory); var rulesSucceeded = await Rules.RunAllAsync(targetDirectory); - if (!rulesSucceeded) - throw new InvalidOperationException("Running generation rules failed."); + if (!rulesSucceeded) { + var messages = Rules.Results.Where(result => !result.Succeeded) + .Select(result => result.Message); + throw new InvalidOperationException( + $"Running generation rules failed. Error: {string.Join("\r\n", messages)}"); + } // Capture outputs in memory var sink = new MemorySink(); diff --git a/tests/Test_Qt.DotNet.Generator/Test_AttributeExtensionsBehavior.cs b/tests/Test_Qt.DotNet.Generator/Test_AttributeExtensionsBehavior.cs index 299b3e5..7d4d7bc 100644 --- a/tests/Test_Qt.DotNet.Generator/Test_AttributeExtensionsBehavior.cs +++ b/tests/Test_Qt.DotNet.Generator/Test_AttributeExtensionsBehavior.cs @@ -45,6 +45,15 @@ namespace Test_Qt.DotNet.Generator } """; + private static string SetupSource(string value) => $$""" + using Qt.Quick; + namespace Test + { + [Qt.Quick.QmlElement(Name = "{{value}}")] + public class Foo { } + } + """; + private static readonly Assembly AdapterAssembly = typeof(QmlElementAttribute).Assembly; public TestContext TestContext { get; set; } @@ -105,5 +114,115 @@ namespace Test_Qt.DotNet.Generator var nonGeneric = (string)attr.Property(nameof(QmlElementAttribute.Name)); Assert.AreEqual("Foo", nonGeneric); } + + [TestMethod, + DataRow(""), + DataRow(" Foo"), + DataRow("Foo "), + DataRow("Foo Bar"), + DataRow("foo"), + DataRow("_Foo"), + DataRow("9Foo") + ] + public async Task InvalidQmlElementName_ShouldFailGeneration(string invalid) + { + var source = SetupSource(invalid); + var exception = await Assert.ThrowsExactlyAsync<InvalidOperationException>(() => + TestCodeGenerator.GenerateAsync([source], + sourceRefs: [typeof(QmlElementAttribute).Assembly], + ct: TestContext.CancellationTokenSource.Token) + ); + + Assert.Contains("QmlElement.Name", exception.Message); + Assert.Contains("is invalid.", exception.Message); + } + + [TestMethod, + DataRow("Foo_"), + DataRow("Foo_Bar"), + DataRow("Foo1"), + DataRow("Foo_1") + ] + public async Task ValidQmlElementName_WithUnderscoresOrNumIsAllowed(string value) + { + var source = SetupSource(value); + var result = await TestCodeGenerator.GenerateAsync( + [source], + sourceRefs: [typeof(QmlElementAttribute).Assembly], + ct: TestContext.CancellationTokenSource.Token); + + Assert.IsNotNull(result); + Assert.IsNotNull(result.SourceAssembly.GetType("Test.Foo")); + } + + [TestMethod] + public async Task ValidName_Emits_QML_NAMED_ELEMENT() + { + const string src = """ + using Qt.Quick; + namespace Test + { + [Qt.Quick.QmlElement(Name = "Foo")] + public class Foo { } + } + """; + + var result = await TestCodeGenerator.GenerateAsync( + [src], sourceRefs: [typeof(QmlElementAttribute).Assembly], + ct: TestContext.CancellationTokenSource.Token); + + Assert.IsTrue(result.Sink.Files.TryGetValue("source/hpp/test/foo.h", out var hpp)); + Assert.Contains("QML_NAMED_ELEMENT(Foo)", hpp); + Assert.DoesNotContain("QML_ELEMENT", hpp); + } + + [TestMethod] + public async Task MissingName_Emits_QML_ELEMENT() + { + const string src = """ + using Qt.Quick; + namespace Test + { + [Qt.Quick.QmlElement] + public class Foo { } + } + """; + + var result = await TestCodeGenerator.GenerateAsync( + [src], sourceRefs: [typeof(QmlElementAttribute).Assembly], + ct: TestContext.CancellationTokenSource.Token); + + Assert.IsTrue(result.Sink.Files.TryGetValue("source/hpp/test/foo.h", out var hpp)); + Assert.Contains("QML_ELEMENT", hpp); + Assert.DoesNotContain("QML_NAMED_ELEMENT(", hpp); + } + + [TestMethod] + public async Task Singleton_Absent_DoesNotEmitMacro_Present_Does() + { + const string src = """ + using Qt.Quick; + namespace Test + { + [Qt.Quick.QmlElement(Name = "Foo")] + public class Foo { } + + [Qt.Quick.QmlElement(Name = "Bar", Singleton = true)] + public class Bar { } + } + """; + + var result = await TestCodeGenerator.GenerateAsync( + [src], sourceRefs: [typeof(QmlElementAttribute).Assembly], + ct: TestContext.CancellationTokenSource.Token); + + Assert.IsTrue(result.Sink.Files.TryGetValue("source/hpp/test/foo.h", out var hpp)); + Assert.Contains("QML_NAMED_ELEMENT(Foo)", hpp); + Assert.DoesNotContain("QML_SINGLETON", hpp); + + Assert.IsTrue(result.Sink.Files.TryGetValue("source/hpp/test/bar.h", out hpp)); + Assert.Contains("QML_NAMED_ELEMENT(Bar)", hpp); + Assert.Contains("QML_SINGLETON", hpp); + } } } |
