This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch edje-vector-intergration
in repository efl.

View the commit online.

commit de057cc47b46bf7c5444fec73052de92ba9c82e1
Author: [email protected] <[email protected]>
AuthorDate: Wed Apr 29 19:31:28 2026 -0600

    edje: wire Phase 7.2 parse-time diagnostics (VEC001/VEC003/VEC011/VEC012)
    
    Task 7.2 consolidates the first four parse-time checks as end-of-tree
    validation. These diagnostics run after each vector or inline-vector
    block is finalized, checking the complete tree structure:
    
    VEC001 (error): Duplicate node names within a tree are now detected via
    _edje_vg_tree_validate_unique_names (which has existed since Phase 2.1
    but was never called). Wired in both _vg_tree_block_end (toplevel) and
    _vg_inline_block_end (per-description inline trees). The message names
    the duplicate node to aid debugging.
    
    VEC003 (error): Invalid SVG path command sequences are rejected at parse
    time. New _edje_vg_path_is_valid() performs lightweight grammar check:
    command letters must be from MmLlHhVvCcSsQqTtAaZz followed by float-ish
    parameter characters and separators. Empty paths accepted; NULL rejected.
    Does not validate per-command parameter counts (left to renderer).
    
    VEC011 (warning): Undeclared color_class names. New _vg_check_class_declared()
    walks the file's color_classes linear list (from edje_file->color_classes).
    Wired into all four color_class setters: node.color_class, shape.fill.color_class,
    shape.stroke_color.color_class, and gradient stop color_class. Empty string
    (sentinel for "no class") suppresses the warning.
    
    VEC012 (warning): Shapes without visible fill or stroke color. New
    _vg_check_shape_color() walks all shapes at end-of-tree via recursive
    _vg_check_tree_shapes_recursive(). Fill is considered "set" if any of:
    color ≠ opaque white (255,255,255,255), color_class name non-empty, or
    gradient_ref non-empty. Stroke warning only fires if stroke_width > 0,
    preventing false positives on shapes with no stroke intent.
    
    Design correction: _edje_vg_node_new() stroke_width default changed from
    1.0 to 0.0. SVG convention: no stroke unless explicitly set. The 1.0 default
    from Phase 2.1 was incorrect; with 0.0, VEC012's stroke_width > 0 check now
    matches author intent. Test helper _node_new_shape() takes explicit stroke_width
    parameter (2.0 in test fixture), so round-trip assertions unaffected. Phase 4.6
    and 5.4 render tests use STATE_SET overrides with explicit stroke.width values.
    
    _compile_with_diag() subprocess test helper (edje_test_vector_states.c):
    Writes EDC to temp file, invokes EFL_RUN_IN_TREE=1 edje_cc via popen with
    merged stderr, scans output for expected diagnostic code, verifies exit
    status (error-level ≠ 0, warning-level = 0). Cleans up temp files on exit.
    
    Four negative-test fixtures added (all 72 tests pass):
    - edje_vg_diag_vec001_node_name_dup: two shapes with same name, must error
    - edje_vg_diag_vec003_invalid_path: path with invalid chars, must error
    - edje_vg_diag_vec011_undeclared_class: references undefined color_class, warns
    - edje_vg_diag_vec012_missing_fill: shape with no fill, color_class, or gradient, warns
    
    Existing test fixtures continue to pass without spurious warnings.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---
 src/bin/edje/edje_cc_handlers.c          | 123 ++++++++++++++++++++++++
 src/lib/edje/edje_vg_tree.c              |  49 +++++++++-
 src/lib/edje/edje_vg_tree.h              |   8 ++
 src/tests/edje/edje_test_vector_states.c | 160 ++++++++++++++++++++++++++++++-
 4 files changed, 335 insertions(+), 5 deletions(-)

diff --git a/src/bin/edje/edje_cc_handlers.c b/src/bin/edje/edje_cc_handlers.c
index 6637e636b3..adf7b29083 100644
--- a/src/bin/edje/edje_cc_handlers.c
+++ b/src/bin/edje/edje_cc_handlers.c
@@ -334,6 +334,9 @@ static void       st_size_class_max(void);
 static void       ob_vectors_vector(void);
 static void       st_vectors_vector_name(void);
 static void       st_vectors_vector_viewbox(void);
+static void       _vg_check_class_declared(const char *name);
+static void       _vg_check_shape_color(const Edje_Vg_Node *n, const char *tree_label);
+static void       _vg_check_tree_shapes_recursive(const Edje_Vg_Node *n, const char *tree_label);
 static void       _vg_tree_block_end(void);
 static void       _vg_open_node(Edje_Vg_Node_Type type);
 static void       ob_vg_shape(void);
@@ -1690,9 +1693,105 @@ ob_vectors_vector(void)
    all_vg_trees = eina_list_append(all_vg_trees, t);
 }
 
+/* --- Phase 7.2 parse-time diagnostics helpers ----------------------------
+ *
+ * These run after a tree is finalised (just before current_vg_tree is
+ * cleared) so they can walk the complete tree.  They emit diagnostics
+ * through edje_diag_emit() which respects -W severity overrides.
+ */
+
+/* VEC011: check that a color_class name is declared in this file. */
+static void
+_vg_check_class_declared(const char *name)
+{
+   if (!name || !*name) return;   /* empty string = "no class" sentinel */
+   Eina_List *l;
+   Edje_Color_Class *cc;
+   EINA_LIST_FOREACH(edje_file->color_classes, l, cc)
+     {
+        if (cc->name && !strcmp(cc->name, name)) return;
+     }
+   edje_diag_emit("VEC011", EDJE_DIAG_WARN, file_in, line, NULL, NULL,
+                  "color_class '%s' is not declared in any color_classes block — "
+                  "embedding apps may register it at runtime via "
+                  "edje_color_class_set()", name);
+}
+
+/* VEC012: walk a SHAPE node and warn if fill/stroke appear unset. */
+static void
+_vg_check_shape_color(const Edje_Vg_Node *n, const char *tree_label)
+{
+   if (!n || n->type != EDJE_VG_NODE_SHAPE) return;
+
+   /* Fill is "set" if any of: color != opaque white, color_class non-empty,
+    * or gradient_ref non-empty.  The all-default case warns. */
+   Eina_Bool fill_set =
+      (n->shape.fill.r != 255 || n->shape.fill.g != 255 ||
+       n->shape.fill.b != 255 || n->shape.fill.a != 255) ||
+      (n->shape.fill.color_class && *n->shape.fill.color_class) ||
+      (n->shape.gradient_ref && *n->shape.gradient_ref);
+
+   if (!fill_set)
+     {
+        edje_diag_emit("VEC012", EDJE_DIAG_WARN, file_in, line, NULL, NULL,
+                       "shape '%s' in %s has no fill color, color_class, or gradient — "
+                       "likely produces unintended output",
+                       n->name ? n->name : "<unnamed>",
+                       tree_label);
+     }
+
+   /* Stroke: warn if stroke_width > 0 but no color/color_class is set. */
+   if (n->shape.stroke_width > 0.0)
+     {
+        Eina_Bool stroke_set =
+           (n->shape.stroke_color.r != 255 || n->shape.stroke_color.g != 255 ||
+            n->shape.stroke_color.b != 255 || n->shape.stroke_color.a != 255) ||
+           (n->shape.stroke_color.color_class &&
+            *n->shape.stroke_color.color_class);
+
+        if (!stroke_set)
+          {
+             edje_diag_emit("VEC012", EDJE_DIAG_WARN, file_in, line, NULL, NULL,
+                            "shape '%s' has stroke_width %.3f but no stroke color "
+                            "or color_class",
+                            n->name ? n->name : "<unnamed>",
+                            (double)n->shape.stroke_width);
+          }
+     }
+}
+
+/* Recursive VEC012 tree walk. */
+static void
+_vg_check_tree_shapes_recursive(const Edje_Vg_Node *n,
+                                 const char *tree_label)
+{
+   if (!n) return;
+   if (n->type == EDJE_VG_NODE_SHAPE)
+     _vg_check_shape_color(n, tree_label);
+   else if (n->type == EDJE_VG_NODE_CONTAINER)
+     {
+        Eina_List *l;
+        Edje_Vg_Node *c;
+        EINA_LIST_FOREACH(n->container.children, l, c)
+          _vg_check_tree_shapes_recursive(c, tree_label);
+     }
+}
+
 static void
 _vg_tree_block_end(void)
 {
+   if (current_vg_tree)
+     {
+        /* VEC001: node-name uniqueness within the toplevel tree. */
+        const char *dup = NULL;
+        if (!_edje_vg_tree_validate_unique_names(current_vg_tree, &dup))
+          edje_diag_emit("VEC001", EDJE_DIAG_ERR, file_in, line, NULL, NULL,
+                         "Duplicate node name '%s' in vector tree", dup);
+
+        /* VEC012: shapes without fill color. */
+        _vg_check_tree_shapes_recursive(current_vg_tree->root,
+                                        "toplevel tree");
+     }
    current_vg_tree = NULL;
    current_vg_node = NULL;
    if (current_vg_node_stack) eina_array_clean(current_vg_node_stack);
@@ -2267,6 +2366,8 @@ st_vg_node_color_class(void)
    char *s = parse_str(0);
    eina_stringshare_replace(&current_vg_node->color.color_class, s);
    free(s);
+   /* VEC011: warn if class is not declared in this file. */
+   _vg_check_class_declared(current_vg_node->color.color_class);
    VG_FLAG(COLOR_CLASS);
 }
 
@@ -2394,6 +2495,10 @@ st_vg_shape_path(void)
    check_arg_count(1);
    EXPECT_SHAPE();
    char *p = parse_str(0);
+   /* VEC003: validate the SVG path command sequence. */
+   if (!_edje_vg_path_is_valid(p))
+     edje_diag_emit("VEC003", EDJE_DIAG_ERR, file_in, line, NULL, NULL,
+                    "path: '%s' is not a valid SVG path command sequence", p);
    eina_stringshare_replace(&current_vg_node->shape.path, p);
    free(p);
    VG_FLAG(PATH);
@@ -2441,6 +2546,8 @@ st_vg_shape_fill_color_class(void)
    char *s = parse_str(0);
    eina_stringshare_replace(&current_vg_node->shape.fill.color_class, s);
    free(s);
+   /* VEC011: warn if class is not declared in this file. */
+   _vg_check_class_declared(current_vg_node->shape.fill.color_class);
    VG_FLAG(FILL_CLASS);
 }
 
@@ -2508,6 +2615,8 @@ st_vg_shape_stroke_color_class(void)
    char *s = parse_str(0);
    eina_stringshare_replace(&current_vg_node->shape.stroke_color.color_class, s);
    free(s);
+   /* VEC011: warn if class is not declared in this file (empty string = clear). */
+   _vg_check_class_declared(current_vg_node->shape.stroke_color.color_class);
    VG_FLAG(STROKE_CLASS);
 }
 
@@ -2912,6 +3021,8 @@ st_vg_grad_stop_color_class(void)
    char *str = parse_str(0);
    eina_stringshare_replace(&_current_stop()->color.color_class, str);
    free(str);
+   /* VEC011: warn if class is not declared in this file. */
+   _vg_check_class_declared(_current_stop()->color.color_class);
 }
 
 #define PROGRAM_OBJECTS(PREFIX)                                                                           \
@@ -15381,6 +15492,18 @@ ob_collections_group_parts_part_description_vector(void)
 static void
 _vg_inline_block_end(void)
 {
+   if (current_vg_tree)
+     {
+        /* VEC001: node-name uniqueness within the inline tree. */
+        const char *dup = NULL;
+        if (!_edje_vg_tree_validate_unique_names(current_vg_tree, &dup))
+          edje_diag_emit("VEC001", EDJE_DIAG_ERR, file_in, line, NULL, NULL,
+                         "Duplicate node name '%s' in inline vector tree", dup);
+
+        /* VEC012: shapes without fill color. */
+        _vg_check_tree_shapes_recursive(current_vg_tree->root,
+                                        "inline tree");
+     }
    current_vg_tree = NULL;
    current_vg_node = NULL;
    if (current_vg_node_stack) eina_array_clean(current_vg_node_stack);
diff --git a/src/lib/edje/edje_vg_tree.c b/src/lib/edje/edje_vg_tree.c
index ca941164cc..370e0ea41b 100644
--- a/src/lib/edje/edje_vg_tree.c
+++ b/src/lib/edje/edje_vg_tree.c
@@ -242,7 +242,7 @@ _edje_vg_node_new(Edje_Vg_Node_Type type)
            n->shape.fill.b = n->shape.fill.a = 255;
         n->shape.stroke_color.r = n->shape.stroke_color.g =
            n->shape.stroke_color.b = n->shape.stroke_color.a = 255;
-        n->shape.stroke_width = 1.0;
+        n->shape.stroke_width = 0.0;
         n->shape.stroke_cap   = EFL_GFX_CAP_BUTT;
         n->shape.stroke_join  = EFL_GFX_JOIN_MITER;
         n->shape.fill_rule    = EFL_GFX_FILL_RULE_WINDING;
@@ -1974,3 +1974,50 @@ _edje_vg_overrides_for_each_class_name(const Eina_List *overrides,
           }
      }
 }
+
+/* =========================================================================
+ * _edje_vg_path_is_valid  — lightweight SVG path grammar check
+ * =========================================================================
+ *
+ * Validates that the string consists entirely of SVG path commands
+ * ([MmLlHhVvCcSsQqTtAaZz]) followed by float-ish parameters
+ * (digits, '.', '+', '-', 'e', 'E') separated by whitespace/commas.
+ * Does NOT validate parameter counts per command (that is left to the
+ * renderer).  An empty string is accepted as a degenerate (but not
+ * invalid) path.  Returns EINA_FALSE on any unrecognised character.
+ */
+EAPI Eina_Bool
+_edje_vg_path_is_valid(const char *path)
+{
+   if (!path) return EINA_FALSE;
+   if (!*path) return EINA_TRUE;   /* empty path = OK */
+
+   const char *p = path;
+   while (*p)
+     {
+        /* Skip whitespace and commas (separators). */
+        while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',')
+          p++;
+        if (!*p) break;
+
+        /* Expect a command letter. */
+        if (!strchr("MmLlHhVvCcSsQqTtAaZz", *p)) return EINA_FALSE;
+        p++;
+
+        /* Skip parameters until the next command letter or end. */
+        while (*p)
+          {
+             /* Skip whitespace and commas. */
+             while (*p == ' ' || *p == '\t' || *p == '\n' ||
+                    *p == '\r' || *p == ',')
+               p++;
+             if (!*p) break;
+             /* Next command letter — exit param loop. */
+             if (strchr("MmLlHhVvCcSsQqTtAaZz", *p)) break;
+             /* Otherwise must be a float-ish character. */
+             if (!strchr("0123456789.+-eE", *p)) return EINA_FALSE;
+             p++;
+          }
+     }
+   return EINA_TRUE;
+}
diff --git a/src/lib/edje/edje_vg_tree.h b/src/lib/edje/edje_vg_tree.h
index af3e09a074..819fbddc4b 100644
--- a/src/lib/edje/edje_vg_tree.h
+++ b/src/lib/edje/edje_vg_tree.h
@@ -212,6 +212,14 @@ EAPI void _edje_vg_overrides_for_each_class_name(const Eina_List *overrides,
                                                  Edje_Vg_Class_Cb cb,
                                                  void *data);
 
+/* --- Phase 7.2: parse-time path validation -------------------------------- */
+
+/* Lightweight SVG path grammar check.  Returns EINA_TRUE if path consists
+ * only of valid SVG command letters and float-ish parameter characters.
+ * An empty string is accepted.  NULL returns EINA_FALSE.  Does NOT
+ * validate parameter counts per command — that is left to the renderer. */
+EAPI Eina_Bool _edje_vg_path_is_valid(const char *path);
+
 /* --- Eet descriptor accessor -------------------------------------------- */
 
 /* Returns the static Eet_Data_Descriptor for Edje_Vg_Tree.
diff --git a/src/tests/edje/edje_test_vector_states.c b/src/tests/edje/edje_test_vector_states.c
index 23c51969af..dc27bf0ae6 100644
--- a/src/tests/edje/edje_test_vector_states.c
+++ b/src/tests/edje/edje_test_vector_states.c
@@ -3,6 +3,9 @@
 #endif
 
 #include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
 #include <Eina.h>
 #include <Eet.h>
 #include <Evas.h>
@@ -951,11 +954,11 @@ EFL_START_TEST(edje_vg_render_box_a_hover)
    fail_if(body_node == NULL);
    fail_if(!efl_isa(body_node, EFL_CANVAS_VG_SHAPE_CLASS));
 
-   /* Default: tile_a has no explicit stroke width, so the materialized shape
-    * inherits the node default of 1.0 (set by _edje_vg_node_new). */
+   /* Default: tile_a body has no explicit stroke.width, so stroke_width
+    * defaults to 0.0 (SVG convention: no stroke unless explicitly set). */
    sw = efl_gfx_shape_stroke_width_get(body_node);
-   ck_assert_msg(sw >= 0.5 && sw <= 1.5,
-                 "Default state stroke width %.2f != expected ~1.0", sw);
+   ck_assert_msg(sw == 0.0,
+                 "Default state stroke width %.2f != expected 0.0", sw);
 
    /* Default fill: tile_a body fill.color = (200,0,0,255); node-level color is
     * default white (255,255,255,255).  Combined per spec §3.2:
@@ -1869,6 +1872,150 @@ EFL_START_TEST(edje_vg_render_box_classed_fill)
 }
 EFL_END_TEST
 
+/* =========================================================================
+ * Phase 7.2 — Negative-test subprocess helper and diagnostic tests
+ *
+ * _compile_with_diag() writes a small EDC to a temp file, invokes edje_cc
+ * as a subprocess (via popen), captures combined stderr, and checks that:
+ *   - the expected diagnostic code string appears in the output, AND
+ *   - if expect_failure is EINA_TRUE, the process exited with a non-zero
+ *     status (i.e. an error-level diagnostic was emitted).
+ *
+ * Returns EINA_TRUE if both conditions hold, EINA_FALSE otherwise.
+ * ========================================================================= */
+
+/* Path to the edje_cc binary in the in-tree build directory. */
+#define _EDJE_CC_BIN TESTS_BUILD_DIR "/../../bin/edje/edje_cc"
+
+static Eina_Bool
+_compile_with_diag(const char *edc_src,
+                   const char *expected_code,
+                   Eina_Bool   expect_failure)
+{
+   /* Write EDC to a temp file. */
+   char edc_path[PATH_MAX];
+   snprintf(edc_path, sizeof(edc_path), "/tmp/edje_diag_test_%d.edc",
+            (int)getpid());
+   FILE *f = fopen(edc_path, "w");
+   if (!f) return EINA_FALSE;
+   fputs(edc_src, f);
+   fclose(f);
+
+   /* Build command: redirect stderr to stdout so popen captures it.
+    * Send the output to /dev/null, capture diagnostics via stderr merge. */
+   char cmd[PATH_MAX * 2];
+   snprintf(cmd, sizeof(cmd),
+            "EFL_RUN_IN_TREE=1 %s -beta -fd /tmp '%s' /dev/null 2>&1",
+            _EDJE_CC_BIN, edc_path);
+
+   FILE *p = popen(cmd, "r");
+   if (!p)
+     {
+        unlink(edc_path);
+        return EINA_FALSE;
+     }
+
+   /* Read all output and look for expected_code. */
+   char buf[4096];
+   Eina_Bool found = EINA_FALSE;
+   while (fgets(buf, sizeof(buf), p))
+     {
+        if (strstr(buf, expected_code))
+          found = EINA_TRUE;
+     }
+
+   int exit_status = pclose(p);
+   unlink(edc_path);
+
+   if (!found) return EINA_FALSE;
+   if (expect_failure && exit_status == 0) return EINA_FALSE;
+   if (!expect_failure && exit_status != 0) return EINA_FALSE;
+   return EINA_TRUE;
+}
+
+/* VEC001 — duplicate node name within a tree: must error (non-zero exit). */
+EFL_START_TEST(edje_vg_diag_vec001_node_name_dup)
+{
+   static const char edc[] =
+      "collections {\n"
+      "   vectors {\n"
+      "      vector { name: \"t\";\n"
+      "         shape { name: \"body\"; path: \"M0,0 L10,10\";\n"
+      "                 fill.color: 200 0 0 255; }\n"
+      "         shape { name: \"body\"; path: \"M5,5 L15,15\";\n"
+      "                 fill.color: 0 200 0 255; }\n"
+      "      }\n"
+      "   }\n"
+      "}\n";
+   ck_assert_msg(_compile_with_diag(edc, "VEC001", EINA_TRUE),
+                 "VEC001 (duplicate node name) should be emitted as an error");
+}
+EFL_END_TEST
+
+/* VEC003 — invalid SVG path: must error (non-zero exit). */
+EFL_START_TEST(edje_vg_diag_vec003_invalid_path)
+{
+   static const char edc[] =
+      "collections {\n"
+      "   vectors {\n"
+      "      vector { name: \"t\";\n"
+      "         shape { name: \"body\"; path: \"$$invalid$$\";\n"
+      "                 fill.color: 200 0 0 255; }\n"
+      "      }\n"
+      "   }\n"
+      "}\n";
+   ck_assert_msg(_compile_with_diag(edc, "VEC003", EINA_TRUE),
+                 "VEC003 (invalid SVG path) should be emitted as an error");
+}
+EFL_END_TEST
+
+/* VEC011 — undeclared color_class: must warn but not fail (zero exit). */
+EFL_START_TEST(edje_vg_diag_vec011_undeclared_class)
+{
+   static const char edc[] =
+      "collections {\n"
+      "   vectors {\n"
+      "      vector { name: \"t\";\n"
+      "         shape { name: \"body\"; path: \"M0,0 L10,10\";\n"
+      "                 fill.color_class: \"undeclared_class\"; }\n"
+      "      }\n"
+      "   }\n"
+      "   group { name: \"g\";\n"
+      "      parts { part { name: \"vg\"; type: VECTOR;\n"
+      "         description { state: \"default\" 0.0;\n"
+      "            vector.use: \"t\";\n"
+      "         }\n"
+      "      } }\n"
+      "   }\n"
+      "}\n";
+   ck_assert_msg(_compile_with_diag(edc, "VEC011", EINA_FALSE),
+                 "VEC011 (undeclared color_class) should warn but not fail");
+}
+EFL_END_TEST
+
+/* VEC012 — fill missing on a shape: must warn but not fail (zero exit). */
+EFL_START_TEST(edje_vg_diag_vec012_missing_fill)
+{
+   static const char edc[] =
+      "collections {\n"
+      "   vectors {\n"
+      "      vector { name: \"t\";\n"
+      "         shape { name: \"body\"; path: \"M0,0 L10,10\"; }\n"
+      "      }\n"
+      "   }\n"
+      "   group { name: \"g\";\n"
+      "      parts { part { name: \"vg\"; type: VECTOR;\n"
+      "         description { state: \"default\" 0.0;\n"
+      "            vector.use: \"t\";\n"
+      "         }\n"
+      "      } }\n"
+      "   }\n"
+      "}\n";
+   ck_assert_msg(_compile_with_diag(edc, "VEC012", EINA_FALSE),
+                 "VEC012 (missing fill color) should warn but not fail");
+}
+EFL_END_TEST
+
 void
 edje_test_vector_states(TCase *tc)
 {
@@ -1906,4 +2053,9 @@ edje_test_vector_states(TCase *tc)
    tcase_add_test(tc, edje_vg_override_class_clear);
    /* Phase 6.4 fill × node-level combined color + class resolution end-to-end */
    tcase_add_test(tc, edje_vg_render_box_classed_fill);
+   /* Phase 7.2 parse-time diagnostic emission tests */
+   tcase_add_test(tc, edje_vg_diag_vec001_node_name_dup);
+   tcase_add_test(tc, edje_vg_diag_vec003_invalid_path);
+   tcase_add_test(tc, edje_vg_diag_vec011_undeclared_class);
+   tcase_add_test(tc, edje_vg_diag_vec012_missing_fill);
 }

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to