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(¤t_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(¤t_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(¤t_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(¤t_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.