Documentation
¶
Overview ¶
Package js_converter lowers JavaScript source into gIR for the taint engine in internal/analysis: parse, name every function (collect.go), then lower each one (lower.go).
Parsing uses esbuild's parser through github.com/bytevet/esbuild-jsast -- no cgo, no Node.js, no external process. It is a TREE parse, not a text transform: TypeScript, JSX and ES modules arrive as themselves, and a node's byte offset indexes the source as written, so lineIndex (dialect.go) resolves every Instruction/Function Pos with no sourcemap in the path.
Lowering model ¶
Every function (declaration, expression, or arrow) becomes its own ir.Function with a real CFG -- blocks, preds/succs, on-demand PHI -- built by converters/ssabuild. if/else lowers to a PHI-merged diamond; the loop forms lower to header/body/exit with a back-edge, so loop-carried taint flows through the header PHI; switch becomes a decision cascade with conservative case-to-case fall-through; try/catch adds a conservative exception edge. A branch-free function still emits exactly one block, keeping the engine's linear fast path. Top-level non-function statements collect into one synthetic "<module>" function per file.
The "opaque object" source heuristic ¶
The engine only introduces fresh taint at a CALL/INVOKE whose Callee matches a rule's source glob, but Express-style sources (`req.query.name`) are property reads, not calls. So the FIRST property access off an *opaque* base -- a free/global identifier or a function parameter, whose value originates outside this function -- is lowered as a CALL with the syntactic callee "js:<base>.<field>", as if it were a getter. Later hops in the chain are ordinary FIELD/INDEX instructions, so the engine's existing FIELD/INDEX propagation carries taint through the rest. See emitRootPropertyRead and isOpaqueBase in lower.go.
Such a read carries BOTH a synthetic callee name and its base register (in Call.Value, tagged builtin.member_read), because a parameter is opaque whether it holds a framework request object or ordinary data; see emitRootPropertyRead for why each is needed.
Real call expressions lower to CALL with a syntactic dotted Callee built from the callee expression (identifier / member / string-keyed index chains; anything else is "<dynamic>") -- the name reflects source syntax, never a value resolved through the environment. A chained call (`axios.get(url).then(cb)`) has its inner call lowered first — as the outer call's receiver — so the inner callee and args stay visible even though the outer name collapses to "<dynamic>.then".
Known limitations ¶
All are conservative (they can only over-approximate reachable taint):
- break/continue are imprecise: a switch case falls through to the next, and a labelled loop is lowered as its underlying loop.
- Closures are not modeled -- each function's environment starts with only its parameters, so an enclosing scope's local falls back to a GlobalName. A module-scope `require()` still resolves, because callee names are purely syntactic and never consult the environment.
- Classes are modeled per method ("<Class>.<method>", via collectClass); only non-method class-body statements (fields, static initializers) are unmodeled.
- Destructuring ASSIGNMENT targets, (non-handler) destructured parameters, and a destructured for-in/for-of loop variable are dropped. A destructuring DECLARATION binds flat identifier names: an object pattern per KEY (a field read off the initializer, so `const {a} = req.query` is precise), an array pattern to the whole initializer (element taint == container taint). Nested patterns bind nothing.
- `await x` / `yield x` lower to `x`; the wrapping is a no-op for taint.
- Array/object literals collapse every element's taint into one PHI-merged register rather than tracking it per index/key.
- Logical `&&`/`||`/`??` reuse the bitwise BIN_OP kinds (gIR has no logical counterpart), losing short-circuit semantics but not taint.
Collector coverage ¶
The collector (collect.go) must walk every expression the lowering lowers. A literal it misses is not just unnamed -- the lowering resolves nothing, emits js.unsupported, and its body goes unanalyzed while the file still reports as converted. TestNoUnsupportedInstructions walks whole trees to catch that.
A parameter default is the blind spot: nothing lowers one, so a missed literal there emits no intrinsic at all and only TestCollectsParamDefaultLiteral sees it.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsJSFamily ¶
IsJSFamily reports whether path is a JavaScript-family source file the frontend handles. It is the single source of truth for the extension set: the converter's own directory walk and internal/scan's dispatch/detection table both call it, so an extension added here reaches every caller. A new extension also needs a rung in parseLadder, which is what decides how it is read.
Types ¶
type Converter ¶
Converter lowers JavaScript source files/directories into gIR: the shared frontend.Driver surface (ConvertFile/ConvertInventory/Skipped) over JavaScript's batch hooks (see batch). Directory scans skip any "node_modules" directory (walkignore).
func NewConverter ¶
func NewConverter() *Converter