Skip to content

validateStackExpressions throws on a non-record entry of an object's fields: list — an inline cast the asArray sweeps could not see #15742

Description

@claude

Found while implementing #15636; filed as a finding only, not claimed.

validateStackExpressions throws on a non-record entry of a per-object fields: list. It is the same defect #15552 and #15636 closed elsewhere, but neither card could see it: both were scoped by a grep for the hand-copied asArray helper, and this reader has no helper — it casts inline.

Measured

On 1c1421401, sweeping the whole AUTHORING_RULES table (42 rules) over five non-record shapes per collection:

objects[].fields   throwers=2  [null=2 undefined=2 a string=0 a number=0 an array=0]
                   validateCapabilityReferences, validateStackExpressions

#15636 re-points validate-capability-references.ts, leaving validateStackExpressions as the last thrower on that collection — and it is still there after that change lands:

objects[].fields   throwers=1  [null=1 undefined=1 ...]  validateStackExpressions

Reproduce with lintStackExpressions({ objects: [{ name: 'crm_account', fields: [null, { name: 'amount', type: 'number' }] }] }):

TypeError: Cannot read properties of null (reading 'name')
  at packages/lint/src/validate-expressions.ts:137:70

The line

buildFieldIndex, packages/lint/src/validate-expressions.ts:137:

if (Array.isArray(fields)) names = fields.map(f => (f as AnyRec).name).filter((n): n is string => typeof n === 'string');

Array.isArray proves the LIST; the cast then asserts each MEMBER is a record, which a list out of YAML does not promise — an empty list item deserialises to null, and .name on it throws. The .filter two calls later would have dropped the junk, but it runs after the dereference.

The object level above it is already guarded (buildFieldIndex is fed through recordsOf); only the field level is not. Two sibling readers in the same file get this right and are worth copying rather than reinventing — validate-expressions.ts:235 reads (f as AnyRec)?.name with optional chaining, and fieldEntries at :255 filters before mapping.

Suggested shape

Read the field list through recordsOf from ./object-graph.js, which is where this predicate lives (its docblock argues why an unreadable ARRAY member is dropped whole). That also removes the inline cast, so the module stops re-deciding a settled question.

Worth sweeping the rest of the file in the same pass: :1112 and :1161 cast flow.nodes and graph.nodes the same way, and were not measured here because this sweep drives collections rather than flow graphs.

Context


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions