fix(ecss): isolate mutable boundary data

Deep-copy mutable selector, schema, stylesheet, cascade, and computed-style values at public ownership boundaries while preserving function identity.\n\nVerified with make check: 57/57 ERT, WERROR byte compilation, checkdoc, and diff checks.
This commit is contained in:
Kinneyzhang 2026-08-06 23:57:18 +08:00
parent 9f6025ee12
commit 51feda4685
4 changed files with 245 additions and 45 deletions

View File

@ -114,11 +114,12 @@
(defun ecss--normalize-schema-initial (initial normalizer validator) (defun ecss--normalize-schema-initial (initial normalizer validator)
"Normalize and validate schema INITIAL with NORMALIZER and VALIDATOR." "Normalize and validate schema INITIAL with NORMALIZER and VALIDATOR."
(let ((normalized (funcall normalizer (copy-tree initial)))) (let ((normalized
(funcall normalizer (ecss--copy-boundary-data initial))))
(unless (funcall validator normalized) (unless (funcall validator normalized)
(signal 'ecss-invalid-property-schema (signal 'ecss-invalid-property-schema
(list :invalid-initial initial))) (list :invalid-initial initial)))
normalized)) (ecss--copy-boundary-data normalized)))
(defun ecss--build-property-schema (id options) (defun ecss--build-property-schema (id options)
"Build a validated property schema for ID from OPTIONS." "Build a validated property schema for ID from OPTIONS."
@ -154,7 +155,8 @@
"Return a defensive copy of SCHEMA." "Return a defensive copy of SCHEMA."
(let ((copy (copy-ecss-property-schema schema))) (let ((copy (copy-ecss-property-schema schema)))
(setf (ecss-property-schema-initial copy) (setf (ecss-property-schema-initial copy)
(copy-tree (ecss-property-schema-initial schema))) (ecss--copy-boundary-data
(ecss-property-schema-initial schema)))
copy)) copy))
(defun ecss-schema-set-property (schemas id) (defun ecss-schema-set-property (schemas id)
@ -166,7 +168,7 @@
(defun ecss-schema-set-property-ids (schemas) (defun ecss-schema-set-property-ids (schemas)
"Return property identifiers from SCHEMAS in registration order." "Return property identifiers from SCHEMAS in registration order."
(ecss--schema-set-check schemas) (ecss--schema-set-check schemas)
(copy-sequence (ecss--schema-set-order schemas))) (ecss--copy-boundary-data (ecss--schema-set-order schemas)))
(defun ecss-schema-set-clear (schemas) (defun ecss-schema-set-clear (schemas)
"Remove every property schema from SCHEMAS." "Remove every property schema from SCHEMAS."
@ -254,10 +256,13 @@
(if-let ((expander (and schema (if-let ((expander (and schema
(ecss-property-schema-shorthand schema)))) (ecss-property-schema-shorthand schema))))
(pcase-let* ((`(,raw . ,important) (ecss--unwrap-important value)) (pcase-let* ((`(,raw . ,important) (ecss--unwrap-important value))
(expanded (funcall expander raw))) (expanded
(ecss--copy-boundary-data
(funcall expander
(ecss--copy-boundary-data raw)))))
(ecss--validate-expanded-longhands schemas property expanded) (ecss--validate-expanded-longhands schemas property expanded)
(ecss--tag-important expanded important)) (ecss--tag-important expanded important))
(list property value)))) (list property (ecss--copy-boundary-data value)))))
;;;###autoload ;;;###autoload
(defun ecss-expand-declarations (schemas declarations) (defun ecss-expand-declarations (schemas declarations)
@ -278,8 +283,7 @@ the same expanded longhand, including explicit nil declarations."
(dolist (declarations declaration-groups result) (dolist (declarations declaration-groups result)
(cl-loop for (property value) (cl-loop for (property value)
on (ecss-expand-declarations schemas declarations) by #'cddr on (ecss-expand-declarations schemas declarations) by #'cddr
do (setq result do (setq result (plist-put result property value))))))
(plist-put result property (copy-tree value)))))))
(defun ecss-stylesheet-create () (defun ecss-stylesheet-create ()
"Create an empty independent stylesheet." "Create an empty independent stylesheet."
@ -294,9 +298,14 @@ the same expanded longhand, including explicit nil declarations."
(defun ecss--copy-rule (rule) (defun ecss--copy-rule (rule)
"Return a defensive copy of RULE." "Return a defensive copy of RULE."
(let ((copy (copy-ecss-rule rule))) (let ((copy (copy-ecss-rule rule)))
(setf (ecss-rule-selector copy) (copy-tree (ecss-rule-selector rule)) (setf (ecss-rule-selector copy)
(ecss-rule-declarations copy) (copy-tree (ecss-rule-declarations rule)) (ecss--copy-boundary-data (ecss-rule-selector rule))
(ecss-rule-scope copy) (copy-tree (ecss-rule-scope rule))) (ecss-rule-declarations copy)
(ecss--copy-boundary-data (ecss-rule-declarations rule))
(ecss-rule-layer copy)
(ecss--copy-boundary-data (ecss-rule-layer rule))
(ecss-rule-scope copy)
(ecss--copy-boundary-data (ecss-rule-scope rule)))
copy)) copy))
(defun ecss-stylesheet-rules (stylesheet) (defun ecss-stylesheet-rules (stylesheet)
@ -320,7 +329,7 @@ the same expanded longhand, including explicit nil declarations."
"Return STYLESHEET layers for ORIGIN in declared order. "Return STYLESHEET layers for ORIGIN in declared order.
ORIGIN defaults to `author'." ORIGIN defaults to `author'."
(ecss--stylesheet-check stylesheet) (ecss--stylesheet-check stylesheet)
(copy-tree (ecss--copy-boundary-data
(ecss--origin-layers (ecss--origin-layers
stylesheet (ecss--validate-origin (or origin 'author))))) stylesheet (ecss--validate-origin (or origin 'author)))))
@ -339,12 +348,14 @@ ORIGIN defaults to `author'."
(setq origin (ecss--validate-origin (or origin 'author))) (setq origin (ecss--validate-origin (or origin 'author)))
(unless (and (listp layers) (cl-every (lambda (layer) layer) layers)) (unless (and (listp layers) (cl-every (lambda (layer) layer) layers))
(signal 'ecss-invalid-rule (list :layers layers))) (signal 'ecss-invalid-rule (list :layers layers)))
(let ((result (copy-tree (ecss--origin-layers stylesheet origin)))) (let ((result (ecss--copy-boundary-data
(ecss--origin-layers stylesheet origin))))
(dolist (layer layers) (dolist (layer layers)
(unless (member layer result) (unless (member layer result)
(setq result (append result (list (copy-tree layer)))))) (setq result
(append result (list (ecss--copy-boundary-data layer))))))
(ecss--set-origin-layers stylesheet origin result) (ecss--set-origin-layers stylesheet origin result)
(copy-tree result))) (ecss--copy-boundary-data result)))
(defun ecss--validate-origin (origin) (defun ecss--validate-origin (origin)
"Return ORIGIN when it is a standard cascade origin." "Return ORIGIN when it is a standard cascade origin."
@ -364,7 +375,8 @@ ORIGIN defaults to `author'."
(layers (ecss--origin-layers stylesheet origin))) (layers (ecss--origin-layers stylesheet origin)))
(when (and layer (not (member layer layers))) (when (and layer (not (member layer layers)))
(ecss--set-origin-layers (ecss--set-origin-layers
stylesheet origin (append layers (list (copy-tree layer)))))) stylesheet origin
(append layers (list (ecss--copy-boundary-data layer))))))
(setf (ecss--stylesheet-source-order stylesheet) (setf (ecss--stylesheet-source-order stylesheet)
(ecss-rule-source-order rule) (ecss-rule-source-order rule)
(ecss--stylesheet-rules stylesheet) (ecss--stylesheet-rules stylesheet)
@ -385,8 +397,8 @@ optional selector limiting the rule to a matching subject or ancestor."
(expanded (ecss-expand-declarations schemas declarations)) (expanded (ecss-expand-declarations schemas declarations))
(source-order (1+ (ecss--stylesheet-source-order stylesheet))) (source-order (1+ (ecss--stylesheet-source-order stylesheet)))
(rule (ecss--make-rule (rule (ecss--make-rule
:selector selector :declarations (copy-tree expanded) :selector selector :declarations expanded
:origin origin :layer (copy-tree layer) :origin origin :layer (ecss--copy-boundary-data layer)
:layer-rank (ecss--prospective-layer-rank :layer-rank (ecss--prospective-layer-rank
stylesheet origin layer) stylesheet origin layer)
:scope scope :source-order source-order))) :scope scope :source-order source-order)))
@ -417,12 +429,13 @@ optional selector limiting the rule to a matching subject or ancestor."
SPECIFICITY, DISTANCE, and DECLARATION-ORDER are the match precedence facts." SPECIFICITY, DISTANCE, and DECLARATION-ORDER are the match precedence facts."
(pcase-let ((`(,raw . ,important) (ecss--unwrap-important value))) (pcase-let ((`(,raw . ,important) (ecss--unwrap-important value)))
(ecss--make-candidate (ecss--make-candidate
:property property :value raw :origin (ecss-rule-origin rule) :property property :value (ecss--copy-boundary-data raw)
:origin (ecss-rule-origin rule)
:important important :inline nil :layer (ecss-rule-layer rule) :important important :inline nil :layer (ecss-rule-layer rule)
:layer-rank (ecss-rule-layer-rank rule) :specificity specificity :layer-rank (ecss-rule-layer-rank rule) :specificity specificity
:scope-distance distance :source-order (ecss-rule-source-order rule) :scope-distance distance :source-order (ecss-rule-source-order rule)
:declaration-order declaration-order :declaration-order declaration-order
:selector (copy-tree (ecss-rule-selector rule))))) :selector (ecss--copy-boundary-data (ecss-rule-selector rule)))))
(defun ecss--rule-candidates (rule subject adapter) (defun ecss--rule-candidates (rule subject adapter)
"Return candidates from matching RULE for SUBJECT through ADAPTER." "Return candidates from matching RULE for SUBJECT through ADAPTER."
@ -447,7 +460,8 @@ SPECIFICITY, DISTANCE, and DECLARATION-ORDER are the match precedence facts."
collect collect
(pcase-let ((`(,raw . ,important) (ecss--unwrap-important value))) (pcase-let ((`(,raw . ,important) (ecss--unwrap-important value)))
(ecss--make-candidate (ecss--make-candidate
:property property :value raw :origin 'author :property property :value (ecss--copy-boundary-data raw)
:origin 'author
:important important :inline t :layer nil :layer-rank nil :important important :inline t :layer nil :layer-rank nil
:specificity '(0 0 0) :specificity '(0 0 0)
:scope-distance most-positive-fixnum :scope-distance most-positive-fixnum
@ -560,7 +574,9 @@ SCHEMAS validates inline declarations and ADAPTER exposes SUBJECT."
(defun ecss--resolve-source (value property subject resolver) (defun ecss--resolve-source (value property subject resolver)
"Resolve VALUE for PROPERTY and SUBJECT only through explicit RESOLVER." "Resolve VALUE for PROPERTY and SUBJECT only through explicit RESOLVER."
(if resolver (funcall resolver value property subject) value)) (if resolver
(ecss--copy-boundary-data (funcall resolver value property subject))
value))
(defun ecss--parent-values (parent-style) (defun ecss--parent-values (parent-style)
"Return computed values from PARENT-STYLE." "Return computed values from PARENT-STYLE."
@ -585,8 +601,10 @@ SCHEMAS validates inline declarations and ADAPTER exposes SUBJECT."
(values (ecss--parent-values parent-style))) (values (ecss--parent-values parent-style)))
(if (and (ecss-property-schema-inherits schema) (if (and (ecss-property-schema-inherits schema)
(plist-member values property)) (plist-member values property))
(cons (copy-tree (plist-get values property)) 'inherit) (cons (ecss--copy-boundary-data (plist-get values property)) 'inherit)
(cons (copy-tree (ecss-property-schema-initial schema)) 'initial)))) (cons (ecss--copy-boundary-data
(ecss-property-schema-initial schema))
'initial))))
(defun ecss--wide-default (wide schema parent-style) (defun ecss--wide-default (wide schema parent-style)
"Resolve non-revert WIDE value for SCHEMA using PARENT-STYLE." "Resolve non-revert WIDE value for SCHEMA using PARENT-STYLE."
@ -596,12 +614,16 @@ SCHEMAS validates inline declarations and ADAPTER exposes SUBJECT."
(let ((values (ecss--parent-values parent-style)) (let ((values (ecss--parent-values parent-style))
(property (ecss-property-schema-id schema))) (property (ecss-property-schema-id schema)))
(if (plist-member values property) (if (plist-member values property)
(cons (copy-tree (plist-get values property)) 'inherit) (cons (ecss--copy-boundary-data (plist-get values property))
(cons (copy-tree (ecss-property-schema-initial schema)) 'initial)))) 'inherit)
(cons (ecss--copy-boundary-data
(ecss-property-schema-initial schema))
'initial))))
('unset (if (ecss-property-schema-inherits schema) ('unset (if (ecss-property-schema-inherits schema)
(ecss--wide-default (ecss-wide-value 'inherit) (ecss--wide-default (ecss-wide-value 'inherit)
schema parent-style) schema parent-style)
(cons (copy-tree (ecss-property-schema-initial schema)) (cons (ecss--copy-boundary-data
(ecss-property-schema-initial schema))
'initial))))) 'initial)))))
(defun ecss--candidate-provenance (candidate &rest extra) (defun ecss--candidate-provenance (candidate &rest extra)
@ -609,11 +631,13 @@ SCHEMAS validates inline declarations and ADAPTER exposes SUBJECT."
(append (append
(if candidate (if candidate
(list :source 'declaration (list :source 'declaration
:selector (copy-tree (ecss--candidate-selector candidate)) :selector (ecss--copy-boundary-data
(ecss--candidate-selector candidate))
:origin (ecss--candidate-origin candidate) :origin (ecss--candidate-origin candidate)
:important (and (ecss--candidate-important candidate) t) :important (and (ecss--candidate-important candidate) t)
:inline (and (ecss--candidate-inline candidate) t) :inline (and (ecss--candidate-inline candidate) t)
:layer (copy-tree (ecss--candidate-layer candidate)) :layer (ecss--copy-boundary-data
(ecss--candidate-layer candidate))
:specificity (copy-sequence :specificity (copy-sequence
(ecss--candidate-specificity candidate)) (ecss--candidate-specificity candidate))
:scope-distance (ecss--candidate-scope-distance candidate) :scope-distance (ecss--candidate-scope-distance candidate)
@ -665,7 +689,7 @@ sources. Retain winner facts when PROVENANCE-P is non-nil."
(let ((table (make-hash-table :test #'eq)) provenance) (let ((table (make-hash-table :test #'eq)) provenance)
(cl-loop for (property value) on (ecss--parent-custom-properties parent-style) (cl-loop for (property value) on (ecss--parent-custom-properties parent-style)
by #'cddr by #'cddr
do (puthash property value table) do (puthash property (ecss--copy-boundary-data value) table)
when provenance-p when provenance-p
do (setq provenance do (setq provenance
(plist-put provenance property '(:source inherit)))) (plist-put provenance property '(:source inherit))))
@ -793,9 +817,10 @@ STACK detects cycles and DIAGNOSTICS records them."
(if (eq value ecss--invalid) (if (eq value ecss--invalid)
value value
(let ((normalized (let ((normalized
(funcall (ecss-property-schema-normalizer schema) value))) (funcall (ecss-property-schema-normalizer schema)
(ecss--copy-boundary-data value))))
(if (funcall (ecss-property-schema-validator schema) normalized) (if (funcall (ecss-property-schema-validator schema) normalized)
normalized (ecss--copy-boundary-data normalized)
ecss--invalid)))) ecss--invalid))))
(defun ecss--compute-candidate-value (defun ecss--compute-candidate-value
@ -811,7 +836,7 @@ explicit sources, and DIAGNOSTICS records variable failures."
source schema parent-style custom diagnostics))) source schema parent-style custom diagnostics)))
(cons (if (eq (cdr resolved) 'declaration) (cons (if (eq (cdr resolved) 'declaration)
(ecss--normalize-property-value schema (car resolved)) (ecss--normalize-property-value schema (car resolved))
(copy-tree (car resolved))) (ecss--copy-boundary-data (car resolved)))
(cdr resolved)))) (cdr resolved))))
(defun ecss--fallback-value (schema parent-style) (defun ecss--fallback-value (schema parent-style)
@ -961,27 +986,29 @@ non-nil, is the only function allowed to evaluate caller-owned value sources;
(defun ecss-computed-style-values (style) (defun ecss-computed-style-values (style)
"Return a defensive values plist from computed STYLE." "Return a defensive values plist from computed STYLE."
(ecss--computed-style-check style) (ecss--computed-style-check style)
(copy-tree (ecss--computed-style-values style))) (ecss--copy-boundary-data (ecss--computed-style-values style)))
(defun ecss-computed-style-custom-properties (style) (defun ecss-computed-style-custom-properties (style)
"Return a defensive custom property plist from computed STYLE." "Return a defensive custom property plist from computed STYLE."
(ecss--computed-style-check style) (ecss--computed-style-check style)
(copy-tree (ecss--computed-style-custom-properties style))) (ecss--copy-boundary-data
(ecss--computed-style-custom-properties style)))
(defun ecss-computed-style-active-properties (style) (defun ecss-computed-style-active-properties (style)
"Return actively supplied property identifiers from computed STYLE." "Return actively supplied property identifiers from computed STYLE."
(ecss--computed-style-check style) (ecss--computed-style-check style)
(copy-sequence (ecss--computed-style-active-properties style))) (ecss--copy-boundary-data
(ecss--computed-style-active-properties style)))
(defun ecss-computed-style-provenance (style) (defun ecss-computed-style-provenance (style)
"Return defensive winner provenance from computed STYLE." "Return defensive winner provenance from computed STYLE."
(ecss--computed-style-check style) (ecss--computed-style-check style)
(copy-tree (ecss--computed-style-provenance style))) (ecss--copy-boundary-data (ecss--computed-style-provenance style)))
(defun ecss-computed-style-diagnostics (style) (defun ecss-computed-style-diagnostics (style)
"Return defensive diagnostics from computed STYLE." "Return defensive diagnostics from computed STYLE."
(ecss--computed-style-check style) (ecss--computed-style-check style)
(copy-tree (ecss--computed-style-diagnostics style))) (ecss--copy-boundary-data (ecss--computed-style-diagnostics style)))
(defun ecss-computed-style-value (style property &optional fallback) (defun ecss-computed-style-value (style property &optional fallback)
"Return PROPERTY from computed STYLE, or FALLBACK when absent." "Return PROPERTY from computed STYLE, or FALLBACK when absent."

View File

@ -44,6 +44,23 @@
(defvar ecss--selector-anchor nil (defvar ecss--selector-anchor nil
"Dynamically bound subject anchoring a relative selector.") "Dynamically bound subject anchoring a relative selector.")
(defun ecss--copy-boundary-data (value)
"Return a detached copy of mutable caller boundary VALUE.
Function values retain identity so executable literals remain callable."
(cond
((functionp value) value)
((consp value)
(cons (ecss--copy-boundary-data (car value))
(ecss--copy-boundary-data (cdr value))))
((stringp value) (copy-sequence value))
((bool-vector-p value) (copy-sequence value))
((or (vectorp value) (recordp value))
(let ((copy (copy-sequence value)))
(dotimes (index (length copy))
(aset copy index (ecss--copy-boundary-data (aref copy index))))
copy))
(t value)))
(defun ecss--adapter-callback-p (value) (defun ecss--adapter-callback-p (value)
"Return non-nil when VALUE is a valid adapter callback." "Return non-nil when VALUE is a valid adapter callback."
(functionp value)) (functionp value))
@ -79,9 +96,11 @@ an alist. PARENT and CHILDREN establish its initial tree position."
(when (and parent (not (ecss-subject-p parent))) (when (and parent (not (ecss-subject-p parent)))
(signal 'wrong-type-argument (list 'ecss-subject-p parent))) (signal 'wrong-type-argument (list 'ecss-subject-p parent)))
(let ((subject (ecss--make-subject (let ((subject (ecss--make-subject
:type type :id id :classes (copy-sequence classes) :type (ecss--copy-boundary-data type)
:attributes (copy-tree attributes) :id (ecss--copy-boundary-data id)
:states (copy-sequence states)))) :classes (ecss--copy-boundary-data classes)
:attributes (ecss--copy-boundary-data attributes)
:states (ecss--copy-boundary-data states))))
(ecss-subject-set-children subject children) (ecss-subject-set-children subject children)
(when parent (when parent
(ecss-subject-set-children (ecss-subject-set-children
@ -464,7 +483,7 @@ an alist. PARENT and CHILDREN establish its initial tree position."
(defun ecss-selector-normalize (selector) (defun ecss-selector-normalize (selector)
"Return a validated defensive AST for string or structured SELECTOR." "Return a validated defensive AST for string or structured SELECTOR."
(let ((ast (if (stringp selector) (ecss-selector-parse selector) selector))) (let ((ast (if (stringp selector) (ecss-selector-parse selector) selector)))
(copy-tree (ecss--validate-selector ast)))) (ecss--copy-boundary-data (ecss--validate-selector ast))))
(defun ecss--subject-attribute-cell (subject adapter name) (defun ecss--subject-attribute-cell (subject adapter name)
"Return SUBJECT attribute NAME through ADAPTER." "Return SUBJECT attribute NAME through ADAPTER."

View File

@ -101,6 +101,20 @@
'(app/padding-top 1 app/padding-right 1 '(app/padding-top 1 app/padding-right 1
app/padding-bottom 1 app/padding-left 4 app/color nil))))) app/padding-bottom 1 app/padding-left 4 app/color nil)))))
(ert-deftest ecss-cascade-test-merge-declarations-copies-caller-values ()
(let* ((schemas (ecss-cascade-test--schemas))
(literal (lambda () :literal))
(caller-value (vector (copy-sequence "blue") literal))
(merged (ecss-merge-declarations
schemas (list 'app/payload caller-value)))
(merged-value (plist-get merged 'app/payload)))
(aset (aref merged-value 0) 0 ?X)
(should (equal (aref caller-value 0) "blue"))
(aset (aref caller-value 0) 1 ?Y)
(should (equal (aref merged-value 0) "Xlue"))
(should (eq (aref merged-value 1) literal))
(should (eq (funcall (aref merged-value 1)) :literal))))
(ert-deftest ecss-cascade-test-shorthand-and-longhand-follow-declaration-order () (ert-deftest ecss-cascade-test-shorthand-and-longhand-follow-declaration-order ()
(let ((schemas (ecss-cascade-test--schemas)) (let ((schemas (ecss-cascade-test--schemas))
(subject (ecss-cascade-test--subject))) (subject (ecss-cascade-test--subject)))
@ -419,6 +433,10 @@
(should (= (ecss-cascade-test--value style 'app/width) 9)) (should (= (ecss-cascade-test--value style 'app/width) 9))
(should (equal (ecss-computed-style-diagnostics style) (should (equal (ecss-computed-style-diagnostics style)
'((:type variable-cycle :path (--a --b --a))))) '((:type variable-cycle :path (--a --b --a)))))
(let ((diagnostics (ecss-computed-style-diagnostics style)))
(setcar (plist-get (car diagnostics) :path) 'damaged)
(should (equal (ecss-computed-style-diagnostics style)
'((:type variable-cycle :path (--a --b --a))))))
(should (null (ecss-computed-style-custom-properties style))))) (should (null (ecss-computed-style-custom-properties style)))))
(ert-deftest ecss-cascade-test-parent-plist-is-snapshotted () (ert-deftest ecss-cascade-test-parent-plist-is-snapshotted ()
@ -449,14 +467,18 @@
(should (eq (plist-get facts :fallback) 'initial)) (should (eq (plist-get facts :fallback) 'initial))
(should-not (ecss-computed-style-present-p style 'app/width))))) (should-not (ecss-computed-style-present-p style 'app/width)))))
(ert-deftest ecss-cascade-test-literal-functions-are-never-called () (ert-deftest ecss-cascade-test-literal-functions-remain-callable ()
(let ((schemas (ecss-cascade-test--schemas)) (calls 0)) (let ((schemas (ecss-cascade-test--schemas)) (calls 0))
(let* ((literal (lambda () (cl-incf calls))) (let* ((literal (lambda () (cl-incf calls)))
(style (ecss-compute-style (style (ecss-compute-style
schemas (ecss-cascade-test--subject) schemas (ecss-cascade-test--subject)
:declarations (list 'app/payload literal)))) :declarations (list 'app/payload literal)))
(value (ecss-cascade-test--value style 'app/payload)))
(should (zerop calls)) (should (zerop calls))
(should (functionp (ecss-cascade-test--value style 'app/payload)))))) (should (eq value literal))
(should (functionp value))
(funcall value)
(should (= calls 1)))))
(ert-deftest ecss-cascade-test-explicit-resolver-runs-once () (ert-deftest ecss-cascade-test-explicit-resolver-runs-once ()
(let ((schemas (ecss-cascade-test--schemas)) (calls 0)) (let ((schemas (ecss-cascade-test--schemas)) (calls 0))
@ -582,6 +604,109 @@
(should (equal (ecss-stylesheet-layers stylesheet) (should (equal (ecss-stylesheet-layers stylesheet)
'((theme base))))))) '((theme base)))))))
(ert-deftest ecss-cascade-test-schema-boundaries-copy-mutable-values ()
(let* ((schemas (ecss-schema-set-create))
(caller-initial (vector (copy-sequence "base"))))
(ecss-schema-set-define schemas 'app/value :initial caller-initial)
(aset (aref caller-initial 0) 0 ?X)
(let ((stored (ecss-property-schema-initial
(ecss-schema-set-property schemas 'app/value))))
(should (equal stored ["base"]))
(aset (aref stored 0) 0 ?Y))
(let ((property-ids (ecss-schema-set-property-ids schemas)))
(setcar property-ids 'damaged)
(should (equal (ecss-schema-set-property-ids schemas) '(app/value))))
(should
(equal (ecss-property-schema-initial
(ecss-schema-set-property schemas 'app/value))
["base"]))))
(ert-deftest ecss-cascade-test-stylesheet-snapshots-mutable-inputs ()
(let* ((schemas (ecss-cascade-test--schemas))
(stylesheet (ecss-stylesheet-create))
(selector-token (copy-sequence "target"))
(caller-value (vector (copy-sequence "blue")))
(layer (vector (copy-sequence "theme"))))
(ecss-stylesheet-add-rule
stylesheet schemas (list :class selector-token)
(list 'app/payload caller-value) :layer layer)
(aset selector-token 0 ?X)
(aset (aref caller-value 0) 0 ?X)
(aset (aref layer 0) 0 ?X)
(let ((rule (car (ecss-stylesheet-rules stylesheet))))
(should (equal (ecss-rule-selector rule) '(:class "target")))
(should (equal (ecss-rule-declarations rule)
'(app/payload ["blue"])))
(should (equal (ecss-stylesheet-layers stylesheet) '(["theme"]))))))
(ert-deftest ecss-cascade-test-stylesheet-getters-copy-mutable-values ()
(let ((schemas (ecss-cascade-test--schemas))
(stylesheet (ecss-stylesheet-create)))
(ecss-stylesheet-add-rule
stylesheet schemas ".target" '(app/payload ["blue"])
:layer '["theme"])
(let ((rule (car (ecss-stylesheet-rules stylesheet)))
(layers (ecss-stylesheet-layers stylesheet)))
(aset (aref (plist-get (ecss-rule-declarations rule) 'app/payload) 0)
0 ?Y)
(aset (aref (ecss-rule-layer rule) 0) 0 ?Y)
(aset (aref (car layers) 0) 0 ?Y))
(should (equal (ecss-rule-declarations
(car (ecss-stylesheet-rules stylesheet)))
'(app/payload ["blue"])))
(should (equal (ecss-rule-layer
(car (ecss-stylesheet-rules stylesheet)))
'["theme"]))
(should (equal (ecss-stylesheet-layers stylesheet) '(["theme"])))))
(ert-deftest ecss-cascade-test-computed-boundaries-copy-mutable-values ()
(let* ((schemas (ecss-cascade-test--schemas))
(caller-value (vector (copy-sequence "blue")))
(style (ecss-compute-style
schemas (ecss-cascade-test--subject)
:declarations (list 'app/payload caller-value)))
(returned (plist-get (ecss-computed-style-values style)
'app/payload)))
(aset (aref caller-value 0) 0 ?X)
(should (equal (ecss-cascade-test--value style 'app/payload) ["blue"]))
(aset (aref returned 0) 0 ?Y)
(should (equal (ecss-cascade-test--value style 'app/payload) ["blue"]))
(let ((active (ecss-computed-style-active-properties style)))
(setcar active 'damaged)
(should (equal (ecss-computed-style-active-properties style)
'(app/payload))))))
(ert-deftest ecss-cascade-test-custom-getter-copies-mutable-values ()
(let* ((schemas (ecss-cascade-test--schemas))
(caller-value (vector (copy-sequence "blue")))
(style (ecss-compute-style
schemas (ecss-cascade-test--subject)
:declarations (list '--theme caller-value)))
(returned (plist-get (ecss-computed-style-custom-properties style)
'--theme)))
(aset (aref caller-value 0) 0 ?X)
(should (equal (ecss-computed-style-custom-properties style)
'(--theme ["blue"])))
(aset (aref returned 0) 0 ?Y)
(should (equal (ecss-computed-style-custom-properties style)
'(--theme ["blue"])))))
(ert-deftest ecss-cascade-test-provenance-getter-copies-selector-data ()
(let ((schemas (ecss-cascade-test--schemas))
(stylesheet (ecss-stylesheet-create))
(subject (ecss-cascade-test--subject :classes '("target"))))
(ecss-stylesheet-add-rule stylesheet schemas ".target" '(app/width 1))
(let* ((style (ecss-compute-style
schemas subject :stylesheet stylesheet :provenance t))
(facts (plist-get (ecss-computed-style-provenance style) 'app/width))
(selector (plist-get facts :selector)))
(aset (cadr selector) 0 ?X)
(should
(equal (plist-get
(plist-get (ecss-computed-style-provenance style) 'app/width)
:selector)
'(:class "target"))))))
(ert-deftest ecss-cascade-test-provenance-is-deterministic-and-defensive () (ert-deftest ecss-cascade-test-provenance-is-deterministic-and-defensive ()
(let ((schemas (ecss-cascade-test--schemas)) (let ((schemas (ecss-cascade-test--schemas))
(stylesheet (ecss-stylesheet-create)) (stylesheet (ecss-stylesheet-create))

View File

@ -95,6 +95,35 @@
(should (ecss-selector-match-p '(:class "primary") subject)) (should (ecss-selector-match-p '(:class "primary") subject))
(should (ecss-selector-match-p '(:state "focus") subject)))) (should (ecss-selector-match-p '(:state "focus") subject))))
(ert-deftest ecss-selector-test-normalize-copies-structured-selector-data ()
(let* ((token (copy-sequence "target"))
(selector (list :class token))
(normalized (ecss-selector-normalize selector)))
(aset (cadr normalized) 0 ?X)
(should (equal selector '(:class "target")))
(aset token 1 ?Y)
(should (equal normalized '(:class "Xarget")))))
(ert-deftest ecss-selector-test-subject-snapshots-mutable-fields ()
(let* ((type (copy-sequence "button"))
(id (copy-sequence "save"))
(classes (list (copy-sequence "primary")))
(attributes (list (cons "role" (copy-sequence "submit"))))
(states (list (copy-sequence "active")))
(subject (ecss-subject-create
:type type :id id :classes classes
:attributes attributes :states states)))
(aset type 0 ?X)
(aset id 0 ?X)
(aset (car classes) 0 ?X)
(aset (cdar attributes) 0 ?X)
(aset (car states) 0 ?X)
(should (equal (ecss-subject-type subject) "button"))
(should (equal (ecss-subject-id subject) "save"))
(should (equal (ecss-subject-classes subject) '("primary")))
(should (equal (ecss-subject-attributes subject) '(("role" . "submit"))))
(should (equal (ecss-subject-states subject) '("active")))))
(ert-deftest ecss-selector-test-caller-owned-subject-adapter () (ert-deftest ecss-selector-test-caller-owned-subject-adapter ()
(let* ((root (vector "panel" "root" '("app") nil nil nil nil)) (let* ((root (vector "panel" "root" '("app") nil nil nil nil))
(child (vector "button" "save" '("primary") (child (vector "button" "save" '("primary")