From 7b49a621ccc3e4daa83239a664cc46e70d4f8ba4 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 5 Apr 2026 06:02:40 +0800 Subject: [PATCH] Tighten XCUI schema consistency rules --- docs/plan/XCUI_Phase_Status_2026-04-05.md | 6 +- .../src/Resources/UI/UIDocumentCompiler.cpp | 57 +++++++++++++++ .../Resources/UI/test_ui_schema_document.cpp | 69 +++++++++++++++++++ 3 files changed, 131 insertions(+), 1 deletion(-) diff --git a/docs/plan/XCUI_Phase_Status_2026-04-05.md b/docs/plan/XCUI_Phase_Status_2026-04-05.md index f414ec70..f97a4c26 100644 --- a/docs/plan/XCUI_Phase_Status_2026-04-05.md +++ b/docs/plan/XCUI_Phase_Status_2026-04-05.md @@ -74,7 +74,7 @@ Current gap: - `XCNewEditor` Debug target builds successfully - `core_ui_tests`: `14/14` - `core_ui_style_tests`: `5/5` -- `ui_resource_tests`: `7/7` +- `ui_resource_tests`: `11/11` - `editor_tests` targeted bridge smoke: `3/3` ## Landed This Phase @@ -94,6 +94,10 @@ Current gap: - artifact schema version bump for UI documents - loader/resource accessors and memory accounting - schema compile/load/artifact regression coverage + - schema consistency rules for: + - `allowedValues` only on `enum` + - `documentKind` / `restrictDocumentKind` only on `document` + - explicit `documentKind` required when `restrictDocumentKind=true` - Engine runtime layer added: - `UIScreenPlayer` - `UIDocumentScreenHost` diff --git a/engine/src/Resources/UI/UIDocumentCompiler.cpp b/engine/src/Resources/UI/UIDocumentCompiler.cpp index 51acf154..b04363f8 100644 --- a/engine/src/Resources/UI/UIDocumentCompiler.cpp +++ b/engine/src/Resources/UI/UIDocumentCompiler.cpp @@ -569,6 +569,10 @@ bool BuildSchemaAttributeDefinition( Containers::Array& diagnostics, Containers::String& inOutErrorMessage) { bool valid = true; + bool hasExplicitDocumentKind = false; + bool hasExplicitRestrictDocumentKind = false; + bool hasExplicitAllowedValues = false; + if (!node.children.Empty()) { AppendDiagnostic( sourcePath, @@ -640,6 +644,7 @@ bool BuildSchemaAttributeDefinition( const UIDocumentAttribute* documentKindAttribute = FindAttributeByName(node, "documentKind", "kind"); if (documentKindAttribute != nullptr && !documentKindAttribute->value.Trim().Empty()) { + hasExplicitDocumentKind = true; if (!TryParseDocumentKindValueExtended(documentKindAttribute->value, outDefinition.documentKind)) { AppendDiagnostic( sourcePath, @@ -653,11 +658,23 @@ bool BuildSchemaAttributeDefinition( valid = false; } else if (outDefinition.valueType == UISchemaValueType::Document) { outDefinition.restrictDocumentKind = true; + } else { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + Containers::String("Schema attribute '") + + outDefinition.name + + "' can only declare 'documentKind' for 'document' value types."); + valid = false; } } if (const UIDocumentAttribute* restrictDocumentKindAttribute = FindAttributeByName(node, "restrictDocumentKind"); restrictDocumentKindAttribute != nullptr && !restrictDocumentKindAttribute->value.Trim().Empty()) { + hasExplicitRestrictDocumentKind = true; if (!TryParseBooleanValueExtended( restrictDocumentKindAttribute->value, outDefinition.restrictDocumentKind)) { @@ -671,14 +688,39 @@ bool BuildSchemaAttributeDefinition( restrictDocumentKindAttribute->value + "' for schema attribute 'restrictDocumentKind'."); valid = false; + } else if (outDefinition.valueType != UISchemaValueType::Document) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + Containers::String("Schema attribute '") + + outDefinition.name + + "' can only declare 'restrictDocumentKind' for 'document' value types."); + valid = false; } } if (const UIDocumentAttribute* allowedValuesAttribute = FindAttributeByName(node, "allowedValues", "values"); allowedValuesAttribute != nullptr) { + hasExplicitAllowedValues = true; outDefinition.allowedValues = ParseAllowedValueList(allowedValuesAttribute->value); } + if (hasExplicitAllowedValues && outDefinition.valueType != UISchemaValueType::Enum) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + Containers::String("Schema attribute '") + + outDefinition.name + + "' can only declare 'allowedValues' for 'enum' value types."); + valid = false; + } + if (outDefinition.valueType == UISchemaValueType::Enum && outDefinition.allowedValues.Empty()) { AppendDiagnostic( sourcePath, @@ -692,6 +734,21 @@ bool BuildSchemaAttributeDefinition( valid = false; } + if (outDefinition.valueType == UISchemaValueType::Document && + outDefinition.restrictDocumentKind && + !hasExplicitDocumentKind) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + Containers::String("Schema attribute '") + + outDefinition.name + + "' must declare 'documentKind' when 'restrictDocumentKind' is true."); + valid = false; + } + return valid; } diff --git a/tests/Resources/UI/test_ui_schema_document.cpp b/tests/Resources/UI/test_ui_schema_document.cpp index 7aa4eca1..e215d26d 100644 --- a/tests/Resources/UI/test_ui_schema_document.cpp +++ b/tests/Resources/UI/test_ui_schema_document.cpp @@ -18,6 +18,31 @@ void WriteTextFile(const std::filesystem::path& path, const std::string& content ASSERT_TRUE(static_cast(output)); } +void ExpectSchemaCompileFailure( + const char* testFolderName, + const char* markup, + const char* expectedMessageFragment) { + namespace fs = std::filesystem; + + const fs::path root = fs::temp_directory_path() / testFolderName; + const fs::path schemaPath = root / "invalid.xcschema"; + fs::remove_all(root); + + WriteTextFile(schemaPath, markup); + + UISchemaLoader loader; + UIDocumentCompileResult compileResult = {}; + EXPECT_FALSE(loader.CompileDocument(schemaPath.string().c_str(), compileResult)); + EXPECT_FALSE(compileResult.succeeded); + EXPECT_FALSE(compileResult.errorMessage.Empty()); + ASSERT_FALSE(compileResult.document.diagnostics.Empty()); + + const std::string errorMessage = compileResult.errorMessage.CStr(); + EXPECT_NE(errorMessage.find(expectedMessageFragment), std::string::npos); + + fs::remove_all(root); +} + TEST(UISchemaDocument, CompileAndArtifactLoadPopulateSchemaDefinition) { namespace fs = std::filesystem; @@ -150,4 +175,48 @@ TEST(UISchemaDocument, CompileRejectsDuplicateAttributeDefinitions) { fs::remove_all(root); } +TEST(UISchemaDocument, CompileRejectsAllowedValuesOnNonEnumAttribute) { + ExpectSchemaCompileFailure( + "xc_ui_schema_allowed_values_non_enum_test", + "\n" + " \n" + " \n" + " \n" + "\n", + "allowedValues"); +} + +TEST(UISchemaDocument, CompileRejectsDocumentKindOnNonDocumentAttribute) { + ExpectSchemaCompileFailure( + "xc_ui_schema_document_kind_non_document_test", + "\n" + " \n" + " \n" + " \n" + "\n", + "documentKind"); +} + +TEST(UISchemaDocument, CompileRejectsRestrictDocumentKindOnNonDocumentAttribute) { + ExpectSchemaCompileFailure( + "xc_ui_schema_restrict_non_document_test", + "\n" + " \n" + " \n" + " \n" + "\n", + "restrictDocumentKind"); +} + +TEST(UISchemaDocument, CompileRejectsRestrictDocumentKindWithoutExplicitDocumentKind) { + ExpectSchemaCompileFailure( + "xc_ui_schema_restrict_without_kind_test", + "\n" + " \n" + " \n" + " \n" + "\n", + "must declare 'documentKind'"); +} + } // namespace