Skip to content

Code Analysis Parameter Defaults

Tu Shaokun edited this page Nov 6, 2026 · 2 revisions

JavaScript parameter defaults and call targets

The lexical call producer could point a valid JavaScript default-parameter call at a function-body declaration that is outside the initializer's scope. Draft #1225 now withdraws that false target and keeps the call explicitly unresolved. The fix is propagated through the existing question/default-import/paired-investigation stack; no PR is merged.

Independent expectation

import { f } from './target.mjs';
function entry(value = f()) {
  function f() { return 'body'; }
  return value;
}
console.log(entry());

With target.mjs exporting function f() { return 'import'; }, Node 22 prints import. The original call record instead reports entry.mjs:2:24–27 as same-file-function, targeting the body declaration at entry.mjs:3:3. Removing that body declaration gives a control that both Node and the analyzer correctly associate with the import.

ECMAScript creates a separate environment for body declarations when parameter expressions exist; closures created in the parameter list cannot see those body declarations. The authored execution above independently checks the exact input. FunctionDeclarationInstantiation

TypeScript 5.9.3 supplies the misleading body symbol and emits semantic diagnostic 2373 when checking the JavaScript. That diagnostic does not make this JavaScript invalid. The typed TypeScript variant is rejected by semantic checking; it is not presented here as valid TypeScript. Archify's syntax-only capture must avoid treating that symbol as a reliable lexical target.

The trigger was found in an authored scope audit. A narrow scan of 617 tracked Archify JS/TS files and 874 parameter initializers found no matching direct-body-function collision. There is no observed production incident or prevalence claim.

Correction and preservation

The nine-line guard checks enclosing parameter nodes. If the alleged function declaration lies inside that parameter's function body, the call retains the existing unresolved/no-local-function-binding representation. It does not search by name for an outer replacement. Imported/outer calls, ordinary body calls and declarations within initializer-created function scopes keep their supported bindings. A body-local const probe already resolved correctly, so that working path was not changed.

Two behavioral regressions fail on the exact previous producer and pass with the guard. Independent review found no actionable issue; ten additional probes cover array/computed/destructured parameters, methods, nested defaults, closures, recursion and preserved initializer-created scopes. All 248 producer-layer module tests and 18 focused tests pass on Node 22.23.2/Python 3.13.16. The initial research used Node 22.23.3; primary validation and the public replay use 22.23.2.

Fixed scope Files Calls Candidate result
Authored minimal case and control 3 6 Exactly one false target withdrawn; direct-import control preserved
Archify Labs bf0a29e7664f52973d06a8e6a608df2db1ecbd39 607 112,145 Every call and other fact preserved
EventEmitter3 a650549176ded0d0dcc52e27b03ae5669d7dcd7c 16 1,085 Every call and other fact preserved
Redux src c9e06506f88926e252daf5275495eba0c04bf8e3 17 87 Every call and other fact preserved
dependency-cruiser e6517db02e169f3db5c838bb1565ebd0fefeb452 1,157 12,876 Every call and other fact preserved; ten existing fixture parse errors remain

These are preservation controls, not a general accuracy measure. Source and unpacked archive raw facts match byte for byte in all five scopes. A fresh public fetch passed the same checks and both authored runtime executions; the four target repositories are complete tracked checkouts, including a 163 MB Archify checkout.

Published revisions

Draft Previous head Current head Call model / adapter revision Current local evidence
#1225 producer bfb7a5d56986db5222291630ad4f1159c77db317 32f10f01c2c0a481419d2a8dfef7d2297a437ec8 v1 / 10 248 module tests; five source/archive scopes; fresh public replay
#1226 saved questions a16d94f78d2ae6b0b92d2e3785781d7e1944554a 39ebb6c6d1d967420cfbfd22f35c573cc4145239 v1 / 10 36 focused tests; six question/archive cases
#1227 default imports c4374a78fd57e27c137673e624e4b3aa4ed7c5ff 6140a1c82f2830343ffa9fabf19cd17e700dadff v2 / 11 37 focused tests; three source/archive scopes and three consumer boundaries
#1229 paired questions b6ad7bfc6aaaf47b9f72e8609d96bea008603741 3dda561d5db3323b00e8df20627ada96aec95900 v2 / 11 280 module tests; seven paired/archive cases and two unchanged raw-fact controls

The v1/v2 contracts are unchanged; adapter revisions identify the corrected extraction behavior. Lower #1219/#1220 heads remain unchanged. All four archives were rebuilt twice identically. The paired layer initially retained an observer test's old revision-9 expectation; it was updated to 11 before the final 280-test pass. Prior Wiki receipts remain evidence for their recorded SHAs. Final-head CI completed for #1225, #1226, #1227 and #1229. Each has the same sole README Classic digest regression verified on exact Labs, with matching expected/actual hashes, plus the aggregate Node22 failure. All other executed jobs passed, including Code Analysis, browser, ZIP and package checks. CI is not green.

Public reproduction

Use Git, Node 22, npm and Python 3 in a Unix shell. The script fetches the previous and current producer, checks the candidate archive hash, installs locked dependencies only in temporary analyzer copies and uses four complete pinned target checkouts. Only the inspected authored helpers are executed; target applications are not run. It does not install Archify into a live agent.

Run the fixed producer, runtime and archive comparison
#!/usr/bin/env bash
set -euo pipefail
# Requires Git, Node 22.23.2, npm and unzip; no live installation is modified.
ARCHIFY_REF=${ARCHIFY_REF:-32f10f01c2c0a481419d2a8dfef7d2297a437ec8}
ARCHIFY_ZIP_SHA256=${ARCHIFY_ZIP_SHA256:-9e0516b885517d15f6fe515975f594eb00c17a775b45ec35835e3fb2110bf641}
[[ "$ARCHIFY_REF" =~ ^[0-9a-f]{40}$ ]] || { echo 'ARCHIFY_REF must be a full lowercase commit SHA' >&2; exit 1; }
[[ "$ARCHIFY_ZIP_SHA256" =~ ^[0-9a-f]{64}$ ]] || { echo 'ARCHIFY_ZIP_SHA256 must be lowercase SHA256' >&2; exit 1; }
node -e "if(process.version !== 'v22.23.2') throw Error('Node v22.23.2 required; found '+process.version)"
WORK=$(mktemp -d "${TMPDIR:-/tmp}/archify-default-scope.XXXXXXXX")
echo "Results and checkouts: $WORK"
export EVIDENCE_ROOT="$WORK/results" CASES="$WORK/cases.json"
mkdir -p "$EVIDENCE_ROOT" "$WORK/minimal"
cat > "$WORK/minimal/entry.mjs" <<'ARCHIFY_SOURCE'
import { f } from './target.mjs';
function entry(value = f()) {
  function f() { return 'body'; }
  return value;
}
console.log(entry());
ARCHIFY_SOURCE
cat > "$WORK/minimal/control.mjs" <<'ARCHIFY_SOURCE'
import { f } from './target.mjs';
function control(value = f()) { return value; }
console.log(control());
ARCHIFY_SOURCE
cat > "$WORK/minimal/target.mjs" <<'ARCHIFY_SOURCE'
export function f() { return 'import'; }
ARCHIFY_SOURCE
cat > "$WORK/public-verify.mjs" <<'ARCHIFY_VERIFY'
import fs from 'node:fs';
import path from 'node:path';
import assert from 'node:assert/strict';
import {spawnSync} from 'node:child_process';
const e=process.env.EVIDENCE_ROOT;
const label=process.argv[2], cli=process.argv[3];
assert.ok(label&&cli,'label and CLI required');
const projects=JSON.parse(fs.readFileSync(process.env.CASES));
const run=(cmd,args)=>{const r=spawnSync(cmd,args,{encoding:'utf8',timeout:240000,maxBuffer:2*1024*1024});assert.equal(r.status,0,r.stderr||r.stdout||String(r.error));return r.stdout;};
const strip=({repository:{adapterRevision,snapshotId,...repository},...facts})=>({repository,...facts});
const result=[];
for(const p of projects){
 if(p.revision){assert.equal(run('git',['-C',p.root,'rev-parse','HEAD']).trim(),p.revision);assert.equal(run('git',['-C',p.root,'status','--porcelain','--untracked-files=no']),'');}
 const out=path.join(e,`${p.name}-${label}`);
 fs.writeFileSync(out+'.receipt.json',run(process.execPath,[cli,'code-analysis','check',p.root,'--language','ts','--fail-on','never','--json','--out',out]));
 const raw=fs.readFileSync(out+'/raw-facts.json'),facts=JSON.parse(raw);
 assert.equal(facts.files.length,p.files,p.name+': file scope');
 assert.equal(facts.imports.length,p.imports,p.name+': import count');
 assert.equal(facts.calls.length,p.calls,p.name+': call count');
 assert.equal(facts.parse_errors?.length||0,p.errors,p.name+': parse errors');
 assert.equal(facts.callModel,'js-ts-lexical-v1');assert.equal(facts.repository.adapterRevision,label==='parent'?8:10);
 if(p.name==='minimal'){
  for(const file of ['entry.mjs','control.mjs'])assert.equal(run(process.execPath,[path.join(p.root,file)]).trim(),'import');
  const call=facts.calls.find(c=>c.file==='entry.mjs'&&c.line===2&&c.column===24);
  assert.ok(call);assert.equal(call.resolution,label==='parent'?'same-file-function':'unresolved');
  if(label==='parent')assert.deepEqual(call.target,{name:'f',line:3,column:3});else assert.equal(call.target,undefined);
  const control=facts.calls.find(c=>c.file==='control.mjs'&&c.line===2);
  assert.equal(control.resolution,'imported-function');assert.deepEqual(control.target,{file:'target.mjs',name:'f',line:1,column:1});
 }
 if(label!=='parent'){
  const before=JSON.parse(fs.readFileSync(path.join(e,`${p.name}-parent/raw-facts.json`)));
  if(p.name==='minimal')before.calls=before.calls.map(c=>c.file==='entry.mjs'&&c.line===2&&c.column===24?(({target,...rest})=>({...rest,resolution:'unresolved',reason:'no-local-function-binding'}))(c):c);
  assert.deepEqual(strip(facts),strip(before),`${p.name}: only expected call/revision/snapshot change`);
 }
 if(label==='archive')assert.deepEqual(raw,fs.readFileSync(path.join(e,`${p.name}-candidate/raw-facts.json`)));
 result.push({name:p.name,pin:p.revision,files:facts.files.length,imports:facts.imports.length,calls:facts.calls.length,parseErrors:facts.parse_errors?.length||0,changedCalls:p.name==='minimal'&&label!=='parent'?1:0,archiveParity:label==='archive'});
 console.log(JSON.stringify(result.at(-1)));
}
fs.writeFileSync(path.join(e,label+'-results.json'),JSON.stringify({node:process.version,result},null,2)+'\n');
ARCHIFY_VERIFY
# Execute only the three authored fixture modules.
for name in entry control; do
 output=$(node "$WORK/minimal/$name.mjs")
 test "$output" = import
 echo "$name runtime: $output"
done
fetch_checkout() {
 local url=$1 revision=$2 dest=$3 pattern=$4
 git init -q "$dest"
 git -C "$dest" remote add origin "$url"
 git -C "$dest" config http.version HTTP/1.1
 if [[ -n "$pattern" ]]; then
  git -C "$dest" config core.sparseCheckout true
  printf '%s\n' "$pattern" > "$dest/.git/info/sparse-checkout"
 fi
 git -C "$dest" -c http.version=HTTP/1.1 fetch --quiet --depth=1 --filter=blob:none origin "$revision"
 git -C "$dest" checkout -q --detach FETCH_HEAD
 test "$(git -C "$dest" rev-parse HEAD)" = "$revision"
}
fetch_checkout https://github.com/tt-a1i/archify.git bfb7a5d56986db5222291630ad4f1159c77db317 "$WORK/parent" '/archify/'
fetch_checkout https://github.com/tt-a1i/archify.git "$ARCHIFY_REF" "$WORK/candidate" $'/archify/\n/archify.zip'
node - "$WORK/candidate/archify.zip" "$ARCHIFY_ZIP_SHA256" <<'JS'
const fs=require('node:fs'),crypto=require('node:crypto'),assert=require('node:assert/strict');
const actual=crypto.createHash('sha256').update(fs.readFileSync(process.argv[2])).digest('hex');
assert.equal(actual,process.argv[3]); console.log('archive SHA256:',actual);
JS
mkdir "$WORK/archive"
unzip -q "$WORK/candidate/archify.zip" -d "$WORK/archive"
for dir in "$WORK/parent/archify/modules/code-analysis" "$WORK/candidate/archify/modules/code-analysis" "$WORK/archive/archify/modules/code-analysis"; do
 (cd "$dir" && npm ci --ignore-scripts --no-audit --no-fund)
done
# Complete tracked checkouts preserve the original analysis roots and source scopes.
fetch_checkout https://github.com/tt-a1i/archify.git bf0a29e7664f52973d06a8e6a608df2db1ecbd39 "$WORK/labs" ''
fetch_checkout https://github.com/primus/eventemitter3.git a650549176ded0d0dcc52e27b03ae5669d7dcd7c "$WORK/eventemitter3" ''
fetch_checkout https://github.com/reduxjs/redux.git c9e06506f88926e252daf5275495eba0c04bf8e3 "$WORK/redux" ''
fetch_checkout https://github.com/sverweij/dependency-cruiser.git e6517db02e169f3db5c838bb1565ebd0fefeb452 "$WORK/dependency-cruiser" ''
node - "$WORK" <<'JS'
const fs=require('node:fs'),path=require('node:path'); const w=process.argv[2];
fs.writeFileSync(process.env.CASES,JSON.stringify([
 {name:'minimal',root:path.join(w,'minimal'),files:3,imports:2,calls:6,errors:0},
 {name:'archify',root:path.join(w,'labs'),revision:'bf0a29e7664f52973d06a8e6a608df2db1ecbd39',files:607,imports:3447,calls:112145,errors:0},
 {name:'eventemitter3',root:path.join(w,'eventemitter3'),revision:'a650549176ded0d0dcc52e27b03ae5669d7dcd7c',files:16,imports:87,calls:1085,errors:0},
 {name:'redux',root:path.join(w,'redux/src'),revision:'c9e06506f88926e252daf5275495eba0c04bf8e3',files:17,imports:39,calls:87,errors:0},
 {name:'dependency-cruiser',root:path.join(w,'dependency-cruiser'),revision:'e6517db02e169f3db5c838bb1565ebd0fefeb452',files:1157,imports:1972,calls:12876,errors:10}
]));
JS
node "$WORK/public-verify.mjs" parent "$WORK/parent/archify/bin/archify.mjs"
node "$WORK/public-verify.mjs" candidate "$WORK/candidate/archify/bin/archify.mjs"
node "$WORK/public-verify.mjs" archive "$WORK/archive/archify/bin/archify.mjs"
echo "Verified; results retained at $WORK"

Call evidence · Validation index

Clone this wiki locally