Skip to content

lint: two more flow-node-list readers throw on a non-record member — lintFlowPatterns and collectFlowVariableNames #16751

Description

@baozhoutao

Found while implementing #15793 (which repaired the two validate-expressions.ts casts). Filed as a finding only, not claimed.

#15793's ruling was to extend non-record-object-entry.test.ts with a graph-shaped arm rather than ship a local test, precisely because no sweep could express "a flow's inner node list". The arm was added — and on its first run it caught a reader in a different file that the card never named.

Measured

On 7c12e475, driving the whole AUTHORING_RULES table over flows[].nodes with a junk member beside a valid node:

flows[].nodes  null       threw=[lintFlowPatterns]  invented=0
flows[].nodes  undefined  threw=[lintFlowPatterns]  invented=0
flows[].nodes  a string   threw=[]                  invented=0
flows[].nodes  a number   threw=[]                  invented=0
flows[].nodes  an array   threw=[]                  invented=0
TypeError: Cannot read properties of null (reading 'type')
    at lint-flow-patterns.ts:1430:39   (Array.find)

Reproduce:

lintFlowPatterns({
  objects: [{ name: 'crm_account', fields: [{ name: 'name', type: 'text' }] }],
  flows: [{ name: 'crm_flow', nodes: [null, { id: 'start', type: 'start', config: {} }], edges: [] }],
});

The lines

packages/lint/src/lint-flow-patterns.ts holds the same two spellings #15793 removed from validate-expressions.ts, three times over:

:1426  const nodes = Array.isArray(flow.nodes) ? (flow.nodes as AnyRec[]) : [];   <- throws at :1430
:456   for (const node of graph.nodes as unknown as AnyRec[]) {
:1522  const graphNodes = graph.nodes as unknown as AnyRec[];

A fourth site is in packages/lint/src/flow-variable-scope.ts:

:225   for (const item of graph.nodes) { const flowNode = item as AnyRec;
         if (typeof flowNode.id === 'string' ...)      <- throws on a null member

Note collectFlowVariableNames guards flow.variables members three lines above with if (!item || typeof item !== 'object') continue; and does not guard graph.nodes — the guard exists in the same function, one loop over.

Reachability differs between the two, and that matters for the pin

  • lintFlowPatterns is reachable shallowly — the repro above, an ordinary flow. This is the live half.
  • collectFlowVariableNames is currently MASKED: every nested route into graph.nodes with a non-record member throws earlier inside packages/spec (see the sibling card on collectFlowGraphs). The one input that reaches it is a region nest exactly MAX_REGION_DEPTH (32) deep, where visit pushes the graph and returns before walking its members:
nesting 32 : OK, 33 graph(s); graphs whose nodes hold a NON-RECORD: 1
nesting 33 : OK, 33 graph(s); graphs whose nodes hold a NON-RECORD: 0

So it is real but only observable at the ceiling today. Fixing the spec-side card will UNMASK it at every depth.

Notes for whoever takes it

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions