aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKarsten Heimrich <karsten.heimrich@qt.io>2025-10-31 22:35:46 +0100
committerMiguel Costa <miguel.costa@qt.io>2025-11-10 17:50:42 +0000
commit23334513211ad17cee183825e9d20967ff868b90 (patch)
tree51704b6c8ac8d2763b2999b4110c6d1e06645221
parentfb33a6c15480505a953d7fcb33ad100914c15eea (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>
-rw-r--r--src/Qt.DotNet.GenerationRules/Qt/DotNet/CodeGeneration/Rules/Class/GenerateClass.cs29
-rw-r--r--src/Qt.DotNet.GenerationRules/Qt/DotNet/Extensions/TypeExtensionsForGenerationRules.cs2
-rw-r--r--tests/Test_Qt.DotNet.Generator/Support/TestCodeGenerator.cs8
-rw-r--r--tests/Test_Qt.DotNet.Generator/Test_AttributeExtensionsBehavior.cs119
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);
+ }
}
}