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 67fcd252f89d3629272a4fa45317642a128369a1
Author: [email protected] <[email protected]>
AuthorDate: Thu Apr 30 09:36:42 2026 -0600
edje: implement VEC004 path-topology mismatch diagnostic (Phase 7.5)
Task 7.5 closes Phase 7, the final compile-time validation phase. VEC004
detects when same-named shapes have topology-incompatible SVG paths across
descriptions of the same VECTOR part — an unsupported morph that causes
runtime step-transitions instead of interpolation.
Topology helpers in edje_cc_out.c:
- _path_command_count(path, cmds, max) extracts the sequence of SVG
command letters (MmLlHhVvCcSsQqTtAaZz) from a path string, ignoring
whitespace, coordinates, and punctuation. Stack buffer at 2048 entries
safely handles all real-world paths (~100 commands typical).
- _paths_topology_compatible(a, b) compares command sequences for equality,
treating 'M' and 'm' as distinct (matches EFL's interpolator behavior).
Walker refactored from _vec009_walk to _vec_walk_pair (and _vec009_check_pair
to _vec_check_pair) to unify pair-iteration: VEC009 emits on node-type
mismatch, VEC004 emits on path topology mismatch for SHAPE nodes only. Single
O(n²) pass covers both diagnostics.
Test helper _compile_with_diag_flags now wraps the previous _compile_with_diag
with optional -W extra_flags argument (e.g. "-W vector-topology=error");
backward-compatible wrapper preserves all 77 existing call sites unchanged.
Three new tests:
1. edje_vg_diag_vec004_topology_mismatch: "M L Z" vs "M L L Z" paths across
descriptions; expects VEC004 warning with exit 0.
2. edje_vg_diag_vec004_no_false_positive: same "M L Z" topology, different
coordinates; no warning, clean compile.
3. edje_vg_diag_vec004_werror_escalation: same fixture as (1) but with
-W vector-topology=error flag; exit non-zero with VEC004 in stderr.
All 80 unit tests pass. Phase 7 complete (5 tasks: diagnostic infrastructure,
parse-time VEC001/003/011/012, VEC014 transform-form conflict, VEC007/009
cross-description, and now VEC004 path topology).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---
src/bin/edje/edje_cc_out.c | 116 +++++++++++++++++----
src/tests/edje/edje_test_vector_states.c | 169 +++++++++++++++++++++++++++++--
2 files changed, 257 insertions(+), 28 deletions(-)
diff --git a/src/bin/edje/edje_cc_out.c b/src/bin/edje/edje_cc_out.c
index 3945e3d586..37c6ac204a 100644
--- a/src/bin/edje/edje_cc_out.c
+++ b/src/bin/edje/edje_cc_out.c
@@ -3024,38 +3024,109 @@ data_write_color_class_register(Eet_File *ef)
/* =========================================================================
* Task 7.4: Cross-description vector diagnostics (VEC007, VEC009)
+ * Task 7.5: VEC004 path-topology mismatch
*
* VEC007 (INFO) — Lottie-source description; topology checks skipped.
* VEC009 (WARN) — same-named nodes have different types across descriptions.
+ * VEC004 (WARN) — same-named shapes have topology-incompatible paths.
* VEC010 — deferred: parent-path mismatch; rare in practice, helper
* to compute parent-path strings is non-trivial. Noted for
* Phase 8 polish.
* VEC008 — deferred: subsumed by Phase 4.3's VEC002 error.
* ========================================================================= */
+/* --- Task 7.5: path topology helpers -------------------------------------- */
+
+/* Extract the ordered command-letter sequence from an SVG path string into
+ * cmds[0..max-1]. Returns the number of commands written. Coordinate
+ * digits, signs, dots, and exponent letters are NOT counted — only the
+ * single-letter command tokens from the SVG path grammar are recorded.
+ *
+ * 'e'/'E' are skipped because they appear inside floating-point exponents
+ * (e.g. "1.5e-3") as well as at word boundaries where they would be
+ * ambiguous. No legitimate SVG path uses the hypothetical 'e' command, so
+ * skipping them causes no false positives. */
+static unsigned int
+_path_command_count(const char *p, char *cmds, unsigned int max)
+{
+ if (!p) return 0;
+ unsigned int n = 0;
+ for (; *p && n < max; p++)
+ {
+ /* Skip whitespace and comma separators. */
+ if (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',')
+ continue;
+ /* Record recognised SVG command letters. */
+ if (strchr("MmLlHhVvCcSsQqTtAaZz", *p))
+ cmds[n++] = *p;
+ /* Everything else (digits, '.', '+', '-', exponents) is coordinate
+ * data — skip silently. */
+ }
+ return n;
+}
+
+/* Return EINA_TRUE if path_a and path_b have the same command-letter
+ * sequence (same length, same per-position command). Coordinate values
+ * are intentionally ignored — they are expected to differ across states
+ * that are meant to morph. */
+static Eina_Bool
+_paths_topology_compatible(const char *path_a, const char *path_b)
+{
+ /* 2048 commands is an ample upper bound; real paths rarely exceed ~100. */
+ char cmds_a[2048], cmds_b[2048];
+ unsigned int n_a = _path_command_count(path_a, cmds_a, sizeof(cmds_a));
+ unsigned int n_b = _path_command_count(path_b, cmds_b, sizeof(cmds_b));
+ if (n_a != n_b) return EINA_FALSE;
+ for (unsigned int i = 0; i < n_a; i++)
+ if (cmds_a[i] != cmds_b[i]) return EINA_FALSE;
+ return EINA_TRUE;
+}
/* Recursive walk over tree A's nodes; for each named node also present in
- * tree B, check whether the types match. Emit VEC009 on mismatch. */
+ * tree B:
+ * - emit VEC009 if the node types differ, OR
+ * - emit VEC004 if both nodes are SHAPEs but their path command sequences
+ * are topology-incompatible (different length or different command at
+ * any position). */
static void
-_vec009_walk(const Edje_Vg_Node *na, const Edje_Vg_Tree *tb,
- const char *part_name,
- const char *state_a, const char *state_b)
+_vec_walk_pair(const Edje_Vg_Node *na, const Edje_Vg_Tree *tb,
+ const char *part_name,
+ const char *state_a, const char *state_b)
{
if (!na) return;
if (na->name)
{
Edje_Vg_Node *nb = _edje_vg_tree_node_by_name(tb, na->name);
- if (nb && nb->type != na->type)
+ if (nb)
{
- edje_diag_emit("VEC009", EDJE_DIAG_WARN, NULL, 0,
- part_name, NULL,
- "node \"%s\" has different types across descriptions "
- "\"%s\" (%s) and \"%s\" (%s) — transition will step "
- "instead of interpolating",
- na->name,
- state_a, edje_vg_node_type_str(na->type),
- state_b, edje_vg_node_type_str(nb->type));
+ if (nb->type != na->type)
+ {
+ /* VEC009: same name, different node types. */
+ edje_diag_emit("VEC009", EDJE_DIAG_WARN, NULL, 0,
+ part_name, NULL,
+ "node \"%s\" has different types across descriptions "
+ "\"%s\" (%s) and \"%s\" (%s) — transition will step "
+ "instead of interpolating",
+ na->name,
+ state_a, edje_vg_node_type_str(na->type),
+ state_b, edje_vg_node_type_str(nb->type));
+ }
+ else if (na->type == EDJE_VG_NODE_SHAPE)
+ {
+ /* VEC004: same name, same SHAPE type, but incompatible path
+ * topology — runtime will step instead of morphing. */
+ if (!_paths_topology_compatible(na->shape.path, nb->shape.path))
+ {
+ edje_diag_emit("VEC004", EDJE_DIAG_WARN, NULL, 0,
+ part_name, NULL,
+ "shape \"%s\" has topology-incompatible paths "
+ "across descriptions \"%s\" and \"%s\" — "
+ "transition will step instead of morphing. "
+ "Use -W vector-topology=error to promote.",
+ na->name, state_a, state_b);
+ }
+ }
}
}
@@ -3064,22 +3135,23 @@ _vec009_walk(const Edje_Vg_Node *na, const Edje_Vg_Tree *tb,
Eina_List *l;
Edje_Vg_Node *child;
EINA_LIST_FOREACH(na->container.children, l, child)
- _vec009_walk(child, tb, part_name, state_a, state_b);
+ _vec_walk_pair(child, tb, part_name, state_a, state_b);
}
}
-/* Check one pair of descriptions for VEC009 type mismatches.
+/* Check one pair of descriptions for VEC009 type mismatches and VEC004
+ * path-topology mismatches.
* tree_a/tree_b are the base trees (overrides not applied — documented
* simplification: applying overrides at compile time would require
* duplication; base-tree comparison is the pragmatic v1 approach). */
static void
-_vec009_check_pair(const Edje_Vg_Tree *tree_a, const Edje_Vg_Tree *tree_b,
- const char *part_name,
- const char *state_a, const char *state_b)
+_vec_check_pair(const Edje_Vg_Tree *tree_a, const Edje_Vg_Tree *tree_b,
+ const char *part_name,
+ const char *state_a, const char *state_b)
{
- /* Walk A→B only: type mismatch is symmetric, so one direction catches
+ /* Walk A→B only: mismatches are symmetric, so one direction catches
* all pairs where the name exists in both trees. */
- _vec009_walk(tree_a->root, tree_b, part_name, state_a, state_b);
+ _vec_walk_pair(tree_a->root, tree_b, part_name, state_a, state_b);
}
/* Validate all VECTOR descriptions in a single part. */
@@ -3122,7 +3194,7 @@ _vec_check_part_descriptions(const Edje_Part_Collection *pc,
}
}
- /* VEC009: walk all pairs of tree-based descriptions. */
+ /* VEC009 + VEC004: walk all pairs of tree-based descriptions. */
if (!edje_file->vector_dir) return;
for (i = 0; i < n; i++)
@@ -3152,7 +3224,7 @@ _vec_check_part_descriptions(const Edje_Part_Collection *pc,
const Edje_Vg_Tree *tb =
&edje_file->vector_dir->trees[db->vg.tree_id];
- _vec009_check_pair(ta, tb, ep->name, state_a, state_b);
+ _vec_check_pair(ta, tb, ep->name, state_a, state_b);
}
}
diff --git a/src/tests/edje/edje_test_vector_states.c b/src/tests/edje/edje_test_vector_states.c
index a313a9e3f2..16696b2c84 100644
--- a/src/tests/edje/edje_test_vector_states.c
+++ b/src/tests/edje/edje_test_vector_states.c
@@ -1881,16 +1881,22 @@ EFL_END_TEST
* - if expect_failure is EINA_TRUE, the process exited with a non-zero
* status (i.e. an error-level diagnostic was emitted).
*
+ * extra_flags: optional additional flags passed to edje_cc (e.g. "-W
+ * vector-topology=error"). Pass NULL for the default invocation.
+ *
* 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"
+/* Core implementation — accepts an optional extra_flags string that is
+ * spliced into the edje_cc command line before the input EDC path. */
static Eina_Bool
-_compile_with_diag(const char *edc_src,
- const char *expected_code,
- Eina_Bool expect_failure)
+_compile_with_diag_flags(const char *edc_src,
+ const char *expected_code,
+ Eina_Bool expect_failure,
+ const char *extra_flags)
{
/* Write EDC to a temp file. */
char edc_path[PATH_MAX];
@@ -1902,11 +1908,12 @@ _compile_with_diag(const char *edc_src,
fclose(f);
/* Build command: redirect stderr to stdout so popen captures it.
- * Send the output to /dev/null, capture diagnostics via stderr merge. */
+ * Send the output to /dev/null, capture diagnostics via stderr merge.
+ * extra_flags (if non-NULL) are inserted before the input path. */
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);
+ "EFL_RUN_IN_TREE=1 %s -beta -fd /tmp %s '%s' /dev/null 2>&1",
+ _EDJE_CC_BIN, extra_flags ? extra_flags : "", edc_path);
FILE *p = popen(cmd, "r");
if (!p)
@@ -1933,6 +1940,16 @@ _compile_with_diag(const char *edc_src,
return EINA_TRUE;
}
+/* Convenience wrapper — no extra flags (backwards-compatible signature used
+ * by all pre-Task-7.5 tests). */
+static Eina_Bool
+_compile_with_diag(const char *edc_src,
+ const char *expected_code,
+ Eina_Bool expect_failure)
+{
+ return _compile_with_diag_flags(edc_src, expected_code, expect_failure, NULL);
+}
+
/* VEC001 — duplicate node name within a tree: must error (non-zero exit). */
EFL_START_TEST(edje_vg_diag_vec001_node_name_dup)
{
@@ -2263,6 +2280,142 @@ EFL_START_TEST(edje_vg_diag_vec009_no_false_positive)
}
EFL_END_TEST
+/* =========================================================================
+ * Phase 7.5 — VEC004 path-topology diagnostic tests
+ * ========================================================================= */
+
+/* VEC004 trigger: two descriptions where shape "body" has paths with
+ * different command counts ("M L Z" = 3 commands vs "M L L Z" = 4 commands).
+ * VEC004 must fire as a warning; compilation must succeed (exit 0). */
+EFL_START_TEST(edje_vg_diag_vec004_topology_mismatch)
+{
+ static const char edc[] =
+ "collections {\n"
+ " group { name: \"g\";\n"
+ " parts {\n"
+ " part { name: \"vg\"; type: VECTOR;\n"
+ " description { state: \"default\" 0.0;\n"
+ " vector {\n"
+ " shape { name: \"body\"; path: \"M0,0 L100,100 Z\";\n"
+ " fill.color: 200 0 0 255; }\n"
+ " }\n"
+ " }\n"
+ " description { state: \"hover\" 0.0;\n"
+ " vector {\n"
+ " shape { name: \"body\"; path: \"M50,0 L100,50 L50,100 Z\";\n"
+ " fill.color: 0 200 0 255; }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ "}\n";
+
+ ck_assert_msg(_compile_with_diag(edc, "VEC004", EINA_FALSE),
+ "VEC004 (path topology mismatch across descriptions) should warn "
+ "(exit 0) when same-named shapes have incompatible command sequences");
+}
+EFL_END_TEST
+
+/* VEC004 positive control: two descriptions where shape "body" has the same
+ * command sequence ("M L Z") but different coordinates. No VEC004 should
+ * fire; compilation should succeed cleanly. */
+EFL_START_TEST(edje_vg_diag_vec004_no_false_positive)
+{
+ static const char edc[] =
+ "collections {\n"
+ " group { name: \"g\";\n"
+ " parts {\n"
+ " part { name: \"vg\"; type: VECTOR;\n"
+ " description { state: \"default\" 0.0;\n"
+ " vector {\n"
+ " shape { name: \"body\"; path: \"M0,0 L100,100 Z\";\n"
+ " fill.color: 200 0 0 255; }\n"
+ " }\n"
+ " }\n"
+ " description { state: \"hover\" 0.0;\n"
+ " vector {\n"
+ " shape { name: \"body\"; path: \"M50,50 L75,75 Z\";\n"
+ " fill.color: 0 200 0 255; }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ "}\n";
+
+ /* Compile using popen so we can inspect both exit status and output. */
+ char edc_path[PATH_MAX];
+ snprintf(edc_path, sizeof(edc_path), "/tmp/edje_diag_test_%d_pos.edc",
+ (int)getpid());
+ FILE *fout = fopen(edc_path, "w");
+ if (!fout) return;
+ fputs(edc, fout);
+ fclose(fout);
+
+ char cmd[PATH_MAX * 2];
+ snprintf(cmd, sizeof(cmd),
+ "EFL_RUN_IN_TREE=1 " _EDJE_CC_BIN
+ " -beta -fd /tmp '%s' /dev/null 2>&1",
+ edc_path);
+
+ FILE *p = popen(cmd, "r");
+ Eina_Bool spurious = EINA_FALSE;
+ char buf[4096];
+ if (p)
+ {
+ while (fgets(buf, sizeof(buf), p))
+ {
+ if (strstr(buf, "VEC004"))
+ spurious = EINA_TRUE;
+ }
+ }
+ int exit_status = p ? pclose(p) : -1;
+ unlink(edc_path);
+
+ ck_assert_msg(exit_status == 0,
+ "VEC004 positive control: compilation should succeed");
+ ck_assert_msg(!spurious,
+ "VEC004 positive control: no VEC004 should fire when paths have "
+ "the same command sequence (same topology, different coordinates)");
+}
+EFL_END_TEST
+
+/* VEC004 -W escalation: the same mismatched-topology fixture as
+ * edje_vg_diag_vec004_topology_mismatch, but compiled with
+ * "-W vector-topology=error". The compiler must exit non-zero. */
+EFL_START_TEST(edje_vg_diag_vec004_werror_escalation)
+{
+ static const char edc[] =
+ "collections {\n"
+ " group { name: \"g\";\n"
+ " parts {\n"
+ " part { name: \"vg\"; type: VECTOR;\n"
+ " description { state: \"default\" 0.0;\n"
+ " vector {\n"
+ " shape { name: \"body\"; path: \"M0,0 L100,100 Z\";\n"
+ " fill.color: 200 0 0 255; }\n"
+ " }\n"
+ " }\n"
+ " description { state: \"hover\" 0.0;\n"
+ " vector {\n"
+ " shape { name: \"body\"; path: \"M50,0 L100,50 L50,100 Z\";\n"
+ " fill.color: 0 200 0 255; }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ "}\n";
+
+ ck_assert_msg(
+ _compile_with_diag_flags(edc, "VEC004", EINA_TRUE,
+ "-W vector-topology=error"),
+ "VEC004 with -W vector-topology=error should promote the warning to an "
+ "error and make edje_cc exit non-zero");
+}
+EFL_END_TEST
+
void
edje_test_vector_states(TCase *tc)
{
@@ -2312,4 +2465,8 @@ edje_test_vector_states(TCase *tc)
tcase_add_test(tc, edje_vg_diag_vec007_lottie_info);
tcase_add_test(tc, edje_vg_diag_vec009_type_mismatch);
tcase_add_test(tc, edje_vg_diag_vec009_no_false_positive);
+ /* Phase 7.5 VEC004 path-topology diagnostics */
+ tcase_add_test(tc, edje_vg_diag_vec004_topology_mismatch);
+ tcase_add_test(tc, edje_vg_diag_vec004_no_false_positive);
+ tcase_add_test(tc, edje_vg_diag_vec004_werror_escalation);
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.