Module refinery.lib.scripts.js.strict
Strict-mode early-error detection for the JavaScript parser. The parser is fully permissive and always
produces the sloppy-mode parse tree; strict mode never changes how source is parsed, only which
already-parsed constructs are illegal. This module is therefore a pure post-parse pass: it walks a
parsed tree, threading strictness down through function bodies, class bodies, and "use strict"
prologues, and records a StrictViolation at every construct that would be a SyntaxError if its
enclosing region ran in strict mode. The tree is never altered.
The intended consumer is the reflection transform, which inlines payloads from always-sloppy surfaces
(Function, indirect eval, string timers) and must refuse an inlining that a strict destination would
reject. That wiring is deliberately not part of this module: a payload with no strict violation can still
diverge at runtime, so collect_strict_violations() is necessary but not sufficient for that decision.
Expand source code Browse git
"""
Strict-mode early-error detection for the JavaScript parser. The parser is fully permissive and always
produces the sloppy-mode parse tree; strict mode never changes how source is parsed, only which
already-parsed constructs are illegal. This module is therefore a pure post-parse pass: it walks a
parsed tree, threading strictness down through function bodies, class bodies, and `"use strict"`
prologues, and records a `StrictViolation` at every construct that would be a `SyntaxError` if its
enclosing region ran in strict mode. The tree is never altered.
The intended consumer is the reflection transform, which inlines payloads from always-sloppy surfaces
(`Function`, indirect `eval`, string timers) and must refuse an inlining that a strict destination would
reject. That wiring is deliberately not part of this module: a payload with no strict violation can still
diverge at runtime, so `collect_strict_violations` is necessary but not sufficient for that decision.
"""
from __future__ import annotations
from dataclasses import dataclass
from refinery.lib.scripts import Expression, Node, Statement
from refinery.lib.scripts.js.model import (
JsArrayPattern,
JsArrowFunctionExpression,
JsAssignmentExpression,
JsAssignmentPattern,
JsBlockStatement,
JsCatchClause,
JsClassDeclaration,
JsClassExpression,
JsExpressionStatement,
JsForInStatement,
JsForOfStatement,
JsFunctionDeclaration,
JsFunctionExpression,
JsIdentifier,
JsIfStatement,
JsImportDefaultSpecifier,
JsImportNamespaceSpecifier,
JsImportSpecifier,
JsLabeledStatement,
JsMemberExpression,
JsMethodDefinition,
JsNumericLiteral,
JsObjectPattern,
JsProperty,
JsPropertyDefinition,
JsRestElement,
JsScript,
JsStringLiteral,
JsUnaryExpression,
JsUpdateExpression,
JsVariableDeclaration,
JsVariableDeclarator,
JsVarKind,
JsWithStatement,
strip_parens,
)
@dataclass(frozen=True)
class StrictViolation:
"""
A single strict-mode early error found in an otherwise sloppy-parsed tree. `rule` is a stable slug
naming the violated restriction; `name` carries the offending identifier for the name-based rules and
is empty otherwise. A violation only records that the code at `offset` would be a `SyntaxError` if its
enclosing region ran in strict mode; the parse tree is never changed.
"""
offset: int
rule: str
name: str = ''
def is_leading_zero_number(raw: str) -> bool:
return len(raw) >= 2 and raw[0] == '0' and raw[1] in '0123456789'
def has_octal_string_escape(raw: str) -> bool:
"""
Whether the raw text of a string literal contains a legacy octal or non-octal-decimal escape — a
backslash followed by `1`-`9`, or by `0` immediately followed by another decimal digit. A plain `\\0`
(the NUL escape) is legal, as is an escaped backslash `\\\\`, so the scan tracks escaping and only a
backslash at an even distance from the previous literal character opens an escape.
"""
body = raw[1:-1] if len(raw) >= 2 else raw
i = 0
n = len(body)
while i < n:
if body[i] != '\\':
i += 1
continue
if i + 1 >= n:
break
nxt = body[i + 1]
if nxt in '123456789':
return True
if nxt == '0':
if i + 2 < n and body[i + 2] in '0123456789':
return True
i += 2
continue
i += 2
return False
def is_use_strict(raw: str) -> bool:
return len(raw) >= 2 and raw[1:-1] == 'use strict'
_STRICT_RESERVED = frozenset({
'implements',
'interface',
'let',
'package',
'private',
'protected',
'public',
'static',
'yield',
})
_EVAL_ARGS = frozenset({'eval', 'arguments'})
def _has_use_strict_prologue(stmts: list[Statement]) -> bool:
for stmt in stmts:
if not isinstance(stmt, JsExpressionStatement):
return False
expr = stmt.expression
if not isinstance(expr, JsStringLiteral):
return False
if is_use_strict(expr.raw):
return True
return False
def _child_strictness(node: Node, strict: bool) -> bool:
if isinstance(node, JsScript):
return strict or _has_use_strict_prologue(node.body)
if isinstance(node, (JsClassDeclaration, JsClassExpression)):
return True
if isinstance(node, JsFunctionDeclaration):
body = node.body
elif isinstance(node, JsFunctionExpression):
body = node.body
elif isinstance(node, JsArrowFunctionExpression):
body = node.body
else:
return strict
if isinstance(body, JsBlockStatement):
return strict or _has_use_strict_prologue(body.body)
return strict
def _record_nested_function(stmt: Statement | None, out: list[StrictViolation]) -> None:
if isinstance(stmt, JsFunctionDeclaration):
out.append(StrictViolation(stmt.offset, 'function-in-statement'))
def _check_node(node: Node, strict: bool, out: list[StrictViolation]) -> None:
if not strict:
return
if isinstance(node, JsNumericLiteral):
if is_leading_zero_number(node.raw):
out.append(StrictViolation(node.offset, 'octal-literal'))
elif isinstance(node, JsStringLiteral):
if has_octal_string_escape(node.raw):
out.append(StrictViolation(node.offset, 'octal-escape'))
elif isinstance(node, JsWithStatement):
out.append(StrictViolation(node.offset, 'with-statement'))
elif isinstance(node, JsUnaryExpression):
if node.operator == 'delete':
target = strip_parens(node.operand)
if isinstance(target, JsIdentifier) and target.name != 'super':
out.append(StrictViolation(node.offset, 'delete-of-reference'))
elif isinstance(node, JsIfStatement):
_record_nested_function(node.consequent, out)
_record_nested_function(node.alternate, out)
elif isinstance(node, JsLabeledStatement):
_record_nested_function(node.body, out)
elif isinstance(node, JsForInStatement):
left = node.left
if isinstance(left, JsVariableDeclaration) and left.kind is JsVarKind.VAR:
declarations = left.declarations
if len(declarations) == 1 and declarations[0].init is not None:
out.append(StrictViolation(left.offset, 'for-in-var-init'))
def _target_identifiers(target: Node | None) -> list[JsIdentifier]:
"""
Every identifier bound or assigned by a binding or assignment target, flattening array and object
patterns, defaults, and rest elements down to their leaves. A pattern default value and a computed
property key are references rather than targets, so they are left for the ordinary traversal; only
the names actually bound by the pattern are returned.
"""
result: list[JsIdentifier] = []
stack: list[Node | None] = [target]
while stack:
node = stack.pop()
if isinstance(node, JsIdentifier):
result.append(node)
elif isinstance(node, JsArrayPattern):
stack.extend(node.elements)
elif isinstance(node, JsObjectPattern):
for prop in node.properties:
if isinstance(prop, JsProperty):
stack.append(prop.value)
elif isinstance(prop, JsRestElement):
stack.append(prop.argument)
elif isinstance(node, JsAssignmentPattern):
stack.append(node.left)
elif isinstance(node, JsRestElement):
stack.append(node.argument)
return result
def _is_property_name_position(node: JsIdentifier) -> bool:
parent = node.parent
if isinstance(parent, JsMemberExpression):
return parent.property is node and not parent.computed
if isinstance(parent, (JsProperty, JsMethodDefinition, JsPropertyDefinition)):
return parent.key is node and not parent.computed
return False
def _flag_name(ident: JsIdentifier, out: list[StrictViolation]) -> None:
if ident.name in _EVAL_ARGS:
out.append(StrictViolation(ident.offset, 'eval-arguments-target', ident.name))
elif ident.name in _STRICT_RESERVED:
out.append(StrictViolation(ident.offset, 'reserved-word', ident.name))
def _flag_bound(target: Node | None, strict: bool, out: list[StrictViolation], handled: set[int]) -> None:
for ident in _target_identifiers(target):
handled.add(id(ident))
if strict:
_flag_name(ident, out)
def _check_parameters(
params: list[Expression],
strict: bool,
out: list[StrictViolation],
handled: set[int],
) -> None:
seen: set[str] = set()
for param in params:
for ident in _target_identifiers(param):
handled.add(id(ident))
if not strict:
continue
_flag_name(ident, out)
if ident.name in seen:
out.append(StrictViolation(ident.offset, 'duplicate-parameter', ident.name))
else:
seen.add(ident.name)
def _check_names(
node: Node,
cur_strict: bool,
child_strict: bool,
out: list[StrictViolation],
handled: set[int],
) -> None:
if isinstance(node, (JsFunctionDeclaration, JsFunctionExpression)):
_check_parameters(node.params, child_strict, out, handled)
_flag_bound(node.id, child_strict, out, handled)
elif isinstance(node, JsArrowFunctionExpression):
_check_parameters(node.params, child_strict, out, handled)
elif isinstance(node, (JsClassDeclaration, JsClassExpression)):
_flag_bound(node.id, child_strict, out, handled)
elif isinstance(node, JsVariableDeclarator):
_flag_bound(node.id, cur_strict, out, handled)
elif isinstance(node, JsCatchClause):
_flag_bound(node.param, cur_strict, out, handled)
elif isinstance(node, (JsImportSpecifier, JsImportDefaultSpecifier, JsImportNamespaceSpecifier)):
_flag_bound(node.local, cur_strict, out, handled)
elif isinstance(node, JsAssignmentExpression):
_flag_bound(node.left, cur_strict, out, handled)
elif isinstance(node, JsUpdateExpression):
_flag_bound(node.argument, cur_strict, out, handled)
elif isinstance(node, (JsForInStatement, JsForOfStatement)):
if not isinstance(node.left, JsVariableDeclaration):
_flag_bound(node.left, cur_strict, out, handled)
elif isinstance(node, JsIdentifier):
if (
id(node) not in handled
and cur_strict
and node.name in _STRICT_RESERVED
and not _is_property_name_position(node)
):
out.append(StrictViolation(node.offset, 'reserved-word', node.name))
def collect_strict_violations(node: Node, *, strict: bool = False) -> list[StrictViolation]:
"""
Every strict-mode early error in the tree rooted at *node*, in source order. *strict* seeds the
strictness of *node* itself; the pass then forces strict inside class bodies and inside any function
whose body opens with a `"use strict"` directive, so a violation is recorded even when the seed is
sloppy but the offending code sits in an inherently strict region. An empty result means the tree has
no strict-mode parse error; it does not imply the tree behaves identically in strict mode, since some
divergences surface only at runtime.
"""
out: list[StrictViolation] = []
handled: set[int] = set()
stack: list[tuple[Node, bool]] = [(node, strict)]
while stack:
current, current_strict = stack.pop()
child_strict = _child_strictness(current, current_strict)
_check_node(current, current_strict, out)
_check_names(current, current_strict, child_strict, out, handled)
for child in current.children():
stack.append((child, child_strict))
out.sort(key=lambda violation: violation.offset)
return out
Functions
def is_leading_zero_number(raw)-
Expand source code Browse git
def is_leading_zero_number(raw: str) -> bool: return len(raw) >= 2 and raw[0] == '0' and raw[1] in '0123456789' def has_octal_string_escape(raw)-
Whether the raw text of a string literal contains a legacy octal or non-octal-decimal escape — a backslash followed by
1-9, or by0immediately followed by another decimal digit. A plain\0(the NUL escape) is legal, as is an escaped backslash\\, so the scan tracks escaping and only a backslash at an even distance from the previous literal character opens an escape.Expand source code Browse git
def has_octal_string_escape(raw: str) -> bool: """ Whether the raw text of a string literal contains a legacy octal or non-octal-decimal escape — a backslash followed by `1`-`9`, or by `0` immediately followed by another decimal digit. A plain `\\0` (the NUL escape) is legal, as is an escaped backslash `\\\\`, so the scan tracks escaping and only a backslash at an even distance from the previous literal character opens an escape. """ body = raw[1:-1] if len(raw) >= 2 else raw i = 0 n = len(body) while i < n: if body[i] != '\\': i += 1 continue if i + 1 >= n: break nxt = body[i + 1] if nxt in '123456789': return True if nxt == '0': if i + 2 < n and body[i + 2] in '0123456789': return True i += 2 continue i += 2 return False def is_use_strict(raw)-
Expand source code Browse git
def is_use_strict(raw: str) -> bool: return len(raw) >= 2 and raw[1:-1] == 'use strict' def collect_strict_violations(node, *, strict=False)-
Every strict-mode early error in the tree rooted at node, in source order. strict seeds the strictness of node itself; the pass then forces strict inside class bodies and inside any function whose body opens with a
"use strict"directive, so a violation is recorded even when the seed is sloppy but the offending code sits in an inherently strict region. An empty result means the tree has no strict-mode parse error; it does not imply the tree behaves identically in strict mode, since some divergences surface only at runtime.Expand source code Browse git
def collect_strict_violations(node: Node, *, strict: bool = False) -> list[StrictViolation]: """ Every strict-mode early error in the tree rooted at *node*, in source order. *strict* seeds the strictness of *node* itself; the pass then forces strict inside class bodies and inside any function whose body opens with a `"use strict"` directive, so a violation is recorded even when the seed is sloppy but the offending code sits in an inherently strict region. An empty result means the tree has no strict-mode parse error; it does not imply the tree behaves identically in strict mode, since some divergences surface only at runtime. """ out: list[StrictViolation] = [] handled: set[int] = set() stack: list[tuple[Node, bool]] = [(node, strict)] while stack: current, current_strict = stack.pop() child_strict = _child_strictness(current, current_strict) _check_node(current, current_strict, out) _check_names(current, current_strict, child_strict, out, handled) for child in current.children(): stack.append((child, child_strict)) out.sort(key=lambda violation: violation.offset) return out
Classes
class StrictViolation (offset, rule, name='')-
A single strict-mode early error found in an otherwise sloppy-parsed tree.
ruleis a stable slug naming the violated restriction;namecarries the offending identifier for the name-based rules and is empty otherwise. A violation only records that the code atoffsetwould be aSyntaxErrorif its enclosing region ran in strict mode; the parse tree is never changed.Expand source code Browse git
@dataclass(frozen=True) class StrictViolation: """ A single strict-mode early error found in an otherwise sloppy-parsed tree. `rule` is a stable slug naming the violated restriction; `name` carries the offending identifier for the name-based rules and is empty otherwise. A violation only records that the code at `offset` would be a `SyntaxError` if its enclosing region ran in strict mode; the parse tree is never changed. """ offset: int rule: str name: str = ''Instance variables
var offset-
The type of the None singleton.
var rule-
The type of the None singleton.
var name-
The type of the None singleton.