js_converter

package
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 23, 2026 License: MIT Imports: 16 Imported by: 0

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

func IsJSFamily(path string) bool

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

type Converter struct {
	frontend.Driver[jsFileResult]
}

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL