Module refinery.lib.scripts.js.strict
Where strict mode comes from, and what it forbids. 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, and it owns two things.
The first is the vocabulary of the Directive Prologue: which nodes can hold one (is_prologue_host()),
what a given one holds (directive_prologue()), whether it declares the Use Strict Directive
(declares_use_strict()), and which mode any node consequently runs in (strict_mode_at()). Directive-hood
is a fact about a statement's position in a statement list, and a deobfuscator rewrites statement
lists constantly, so every pass that moves, inserts, removes or folds a statement must ask the same
question of the same names — a pass that re-derives the rules is a pass that gets a different answer.
The second is the early errors: collect_strict_violations() walks a parsed tree, threading strictness
down through function bodies, class bodies and prologues, and records a StrictViolation at every
construct the language refuses. The tree is never altered. Most of those constructs are refused only
by a strict region, which is what the seeded mode is for; several are refused whatever mode the program
runs in, and are named at StrictViolation.
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
"""
Where strict mode comes from, and what it forbids. 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, and it owns two things.
The first is the vocabulary of the Directive Prologue: which nodes can hold one (`is_prologue_host`),
what a given one holds (`directive_prologue`), whether it declares the Use Strict Directive
(`declares_use_strict`), and which mode any node consequently runs in (`strict_mode_at`). Directive-hood
is a fact about a statement's *position in a statement list*, and a deobfuscator rewrites statement
lists constantly, so every pass that moves, inserts, removes or folds a statement must ask the same
question of the same names — a pass that re-derives the rules is a pass that gets a different answer.
The second is the early errors: `collect_strict_violations` walks a parsed tree, threading strictness
down through function bodies, class bodies and prologues, and records a `StrictViolation` at every
construct the language refuses. The tree is never altered. Most of those constructs are refused only
by a strict region, which is what the seeded mode is for; several are refused whatever mode the program
runs in, and are named at `StrictViolation`.
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
import enum
from dataclasses import dataclass
from refinery.lib.scripts import Node, Statement
from refinery.lib.scripts.js.lexer import has_legacy_numeric_escape
from refinery.lib.scripts.js.model import (
FUNCTION_NODES,
JsArrayPattern,
JsArrowFunctionExpression,
JsAssignmentExpression,
JsAssignmentPattern,
JsAwaitExpression,
JsBlockStatement,
JsCatchClause,
JsClassDeclaration,
JsClassExpression,
JsExportAllDeclaration,
JsExportDefaultDeclaration,
JsExportNamedDeclaration,
JsExpressionStatement,
JsForInStatement,
JsForOfStatement,
JsFunctionDeclaration,
JsFunctionExpression,
JsFunctionNode,
JsIdentifier,
JsIfStatement,
JsImportDeclaration,
JsImportDefaultSpecifier,
JsImportNamespaceSpecifier,
JsImportSpecifier,
JsLabeledStatement,
JsMetaProperty,
JsMethodDefinition,
JsMethodKind,
JsNumericLiteral,
JsObjectPattern,
JsProperty,
JsPropertyKind,
JsRestElement,
JsScript,
JsStaticBlock,
JsStringLiteral,
JsUnaryExpression,
JsUpdateExpression,
JsVariableDeclaration,
JsVariableDeclarator,
JsVarKind,
JsWithStatement,
JsYieldExpression,
names_a_property,
strip_parens,
)
@dataclass(frozen=True)
class StrictViolation:
"""
A single 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. The parse tree is never changed.
Most rules record that the code at `offset` would be a `SyntaxError` if its enclosing region ran in
strict mode. Four do not: a Use Strict Directive under a parameter list that is not simple, a
repeated name in a list the grammar requires to be unique, the arity of an accessor, and a name
reserved by the kind of function it stands in are refused in *either* mode, so a caller that treats
an empty result as "sloppy code is safe" is reading it right, and one that treats a non-empty
result as "only strict code would refuse this" is not.
"""
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(node: JsStringLiteral) -> bool:
"""
Whether a string literal was written with an escape that strict code rejects. It is the same
spelling a template excludes from its grammar, so the scan itself lives beside the escapes it
reads and both rules ask it there.
"""
return has_legacy_numeric_escape(node.body)
def is_use_strict(node: JsStringLiteral) -> bool:
"""
Whether a literal spells the Use Strict Directive. It is asked of the spelling rather than of
the value, because a directive is one: a literal that denotes `use strict` through an escape is
not the directive, and neither is one the source never closed.
"""
return node.terminated and node.body == 'use strict'
def spelling_states(body: str) -> tuple[bool, bool]:
"""
What a literal's spelling states, as against what it denotes: whether it is the Use Strict
Directive, and whether it carries an escape strict code rejects. Both are facts about how the
literal was written and about nothing else, so a pass that re-spells one may do so only where
neither answer moves — re-spelling `'use\\x20strict'` as `'use strict'` writes a directive the
source never wrote, and every line behind it becomes strict code.
"""
return body == 'use strict', has_legacy_numeric_escape(body)
def statement_list(node: Node | None) -> list[Statement] | None:
"""
The statement list *node* holds directly, or `None` when it holds none. Only the three node types
that can host a Directive Prologue are answered for, so a caller that already knows it is looking
at a host reads the list here without consulting the tree above it.
"""
if isinstance(node, (JsScript, JsBlockStatement, JsStaticBlock)):
return node.body
return None
def is_prologue_host(node: Node | None) -> bool:
"""
Whether *node* holds a statement list that a Directive Prologue can open (§11.2.1): a script body,
a function body, or a class static block. Nothing else does. A plain block, the body of a `try`,
`catch` or `finally`, a labelled statement, a `switch` case and the expression body of a concise
arrow all hold code no directive governs, so a `'use strict'` written at the head of one is an
ordinary string-valued statement that changes no mode.
A function body is recognized through the function that owns it, because a body and a plain block
are the same node type and only the tree above tells them apart.
"""
if isinstance(node, (JsScript, JsStaticBlock)):
return True
if isinstance(node, JsBlockStatement):
owner = node.parent
return isinstance(owner, FUNCTION_NODES) and owner.body is node
return False
def directive_prologue(host: Node | None) -> list[JsExpressionStatement]:
"""
The Directive Prologue of *host*: the run of statements it opens with that consist of nothing but
a string literal. The run ends at the first statement that is anything else, so it is a prefix, and
every statement behind that one is ordinary code however it happens to be spelled.
A parenthesized literal is not one of them. A directive is a statement whose expression *is* the
literal, so `('use strict');` states nothing, and the parser keeps the parenthesis as a node of its
own precisely so that this stays decidable.
*host* is taken to be a prologue host; where a caller must find the host from a statement inside
it, `is_prologue_host` decides that.
"""
return leading_string_statements(statement_list(host) or [])
def leading_string_statements(statements: list[Statement]) -> list[JsExpressionStatement]:
"""
The opening run of *statements* that consist of nothing but a string literal. Where *statements*
is a prologue host's own list this is its Directive Prologue; a caller holding the list rather
than the host — the printer, which is handed a body — asks here.
"""
run: list[JsExpressionStatement] = []
for statement in statements:
if not isinstance(statement, JsExpressionStatement):
break
if not isinstance(statement.expression, JsStringLiteral):
break
run.append(statement)
return run
def declares_use_strict(host: Node | None) -> bool:
"""
Whether the Directive Prologue of *host* holds the Use Strict Directive, which makes the code
*host* encloses strict. The directive need not open the prologue: every string-literal statement
ahead of it is a directive too, and one the language does not recognize is simply inert.
It is the same directive `is_use_strict_directive` names, which is what keeps the mode this reports
and the mode the printer writes from parting company. A string an edit lifted to the head of a body
declares nothing — the printer puts it in a bracket precisely so that it cannot — and a body read as
strict on the strength of one would be reasoned about in a mode the text will not have.
"""
return any(is_use_strict_directive(statement) for statement in directive_prologue(host))
def mark_directives(root: Node) -> None:
"""
Record on every statement of every Directive Prologue in the tree at *root* that the source wrote
it where a directive stands. A parser calls this once over the finished tree, which is the only
point at which the question can be answered: a statement is a directive by virtue of the list it
sits in and the statements ahead of it, and neither is known while it is being built.
The mark is provenance and never a conclusion. Whether a marked statement still *is* a directive
is asked of where it stands now; what the mark adds is the other half, that a statement standing
at the head of a body today was not merely put there by an edit.
"""
for node in root.walk():
if is_prologue_host(node):
for statement in directive_prologue(node):
statement.directive = True
def names_module_syntax(root: Node) -> bool:
"""
Whether the tree at *root* holds syntax only module code may hold: an `import` or `export`
declaration, or `import.meta`. A dynamic `import()` is not among them — it is available to a script
as well — and neither is a top-level `await`, which the parser reads as a name followed by a call
and which would therefore fire on any program that happens to use `await` as an ordinary
identifier.
The answer is a lower bound and never a refutation. §16.1 leaves module-ness to the host, so a
module that spells none of this syntax is one nothing in the text distinguishes from a script.
"""
for node in root.walk():
if isinstance(node, (
JsImportDeclaration,
JsExportAllDeclaration,
JsExportDefaultDeclaration,
JsExportNamedDeclaration,
)):
return True
if isinstance(node, JsMetaProperty) and node.meta == 'import' and node.property == 'meta':
return True
return False
def mark_module(script: JsScript) -> None:
"""
Record on *script* whether its source is module code, as `names_module_syntax` observes it. A parser
calls this once over the finished tree, beside `mark_directives`: both record what the source was,
at the one point where the whole source is in hand and no edit has moved anything yet.
"""
script.module = names_module_syntax(script)
def is_use_strict_directive(statement: Statement) -> bool:
"""
Whether *statement* is the Use Strict Directive, and so the reason some body runs in strict mode.
Three things must hold and each rules out a different mistake.
The source must have written it where a directive stands, or an edit that moved a string here is
credited with a mode the file never declared. It must still stand in a prologue host's opening
run, or a directive carried somewhere else by a clone or a splice is credited with a mode it no
longer declares. And it must spell `use strict`, because every other directive is inert and
deleting one changes nothing.
This is the predicate a removal asks before dropping a statement and an insertion asks before
stepping over one, so that the two cannot disagree about which statement is at stake.
"""
if not isinstance(statement, JsExpressionStatement) or not statement.directive:
return False
expression = statement.expression
if not isinstance(expression, JsStringLiteral) or not is_use_strict(expression):
return False
host = statement.parent
if not is_prologue_host(host):
return False
return any(member is statement for member in directive_prologue(host))
def keeping_directives(host: Node, replacement: list[Statement]) -> list[Statement]:
"""
*replacement* with a Use Strict Directive that *host* currently opens with put back at its head,
for a caller about to install *replacement* as *host*'s whole body. Nothing is carried where
*replacement* already opens with one, whether that is the host's own statement or a copy of it: a
pass that rebuilds a body by cloning it keeps the directive by value and not by identity, and
carrying the original as well would write the directive twice.
A directive is what a body opens with, so one the caller kept but put behind another statement
declares nothing where it now stands and the host's is put in front of it. It is *moved* there
and never copied ahead of itself: a tree holds a node in one place, and a list naming the same
statement at two indices would leave one node with one parent standing at both, which every
`id`-keyed map over the tree then reads as two. Where the statement stood it computed a string
and discarded it, so taking it from there changes nothing but the mode it now declares.
Whole-body replacement is the one way a directive is lost without a removal: nothing is deleted,
the statement is simply absent from the list handed in, so a rule phrased over removals cannot see
it. It is repaired rather than refused because a pass reaches this point having already rewritten
what it is about to install, and declining here would leave those rewrites standing over a body
that never received them.
"""
if not is_prologue_host(host) or _opens_with_use_strict(replacement):
return replacement
carried = next(
(
statement for statement in directive_prologue(host)
if is_use_strict_directive(statement)
),
None,
)
if carried is None:
return replacement
return [carried, *(kept for kept in replacement if kept is not carried)]
def _opens_with_use_strict(statements: list[Statement]) -> bool:
"""
Whether the opening run of *statements* holds a statement the source wrote as the Use Strict
Directive, so that a body built from them declares strict mode wherever it is installed. It is
asked of a list that stands in no tree yet, which is why it reads the mark and the spelling
rather than `is_use_strict_directive`, whose third question is where the statement stands.
"""
return any(
statement.directive
and isinstance(statement.expression, JsStringLiteral)
and is_use_strict(statement.expression)
for statement in leading_string_statements(statements)
)
def promoted_use_strict(statements: list[Statement]) -> list[JsExpressionStatement]:
"""
Which statements of *statements* would be read as the Use Strict Directive without ever having
been written as one. *statements* is a prologue host's own list, so its opening run of
string-literal statements is a Directive Prologue: a member of that run spelling `use strict` and
carrying no mark came to stand there through an edit, and writing it plain makes the body strict
where the source left it sloppy.
None are reported once the run holds a directive the source did write. The body is strict either
way, so there is no mode to save, and a parenthesis there would end the run and eject every real
directive standing behind it — which is how a repair becomes a second defect.
Only `use strict` is reported. Every other promoted string is inert wherever it lands: it declares
no mode, and parenthesizing it would end the run for nothing.
"""
promoted: list[JsExpressionStatement] = []
for statement in leading_string_statements(statements):
expression = statement.expression
if not isinstance(expression, JsStringLiteral) or not is_use_strict(expression):
continue
if statement.directive:
return []
promoted.append(statement)
return promoted
def joins_directive_prologue(statement: Statement) -> bool:
"""
Whether *statement* would enter the Directive Prologue of the body that holds it were it spelled as
a string literal: it sits in a prologue host, and nothing but string-literal statements precede it.
A pass that rewrites such a statement into a literal hands the prologue that statement *and* every
string-literal statement standing behind it, so a `'use strict'` that was ordinary code becomes the
directive that makes the whole body strict.
"""
host = statement.parent
body = statement_list(host)
if body is None or not is_prologue_host(host):
return False
index = len(directive_prologue(host))
return index < len(body) and body[index] is statement
def strict_mode_at(node: Node) -> bool:
"""
Whether the code at *node* runs in strict mode. Mode is inherited (§11.2.2): a body is strict when
its own Directive Prologue declares it or when the code enclosing it is strict, and every part of a
class definition is strict whatever encloses it (§15.7). *node* itself counts, so asking this of a
function body answers the mode that body runs in.
A function's directive reaches further than the body that holds it: the parameter list and the name
the function binds are strict code too, which is why `function f(eval) { 'use strict'; }` is refused
and `function f(eval) {}` is a program. Neither stands inside the body, so the whole function is
asked, not only the host.
Module code is strict throughout (§11.2.2), whatever any body in it declares, so the climb ends by
asking the script it arrives at. What it asks is `mark_module`'s observation of the source, which
only ever reports a module and never denies one: a program the host loads as a module while its
text names no import, export or `import.meta` is read here as a script, and the mode it is given is
the weaker of the two.
"""
cursor: Node | None = node
while cursor is not None:
if isinstance(cursor, JsScript) and cursor.module:
return True
if isinstance(cursor, (JsClassDeclaration, JsClassExpression)):
return True
if is_prologue_host(cursor) and declares_use_strict(cursor):
return True
if isinstance(cursor, FUNCTION_NODES) and declares_use_strict(cursor.body):
return True
cursor = cursor.parent
return False
def has_simple_parameters(fn: JsFunctionNode) -> bool:
"""
Whether *fn* has a simple parameter list (§15.1.3): every parameter is a plain identifier, with no
default, no rest element and no destructuring. An empty list is simple — nothing in it is anything
else — which is what makes a Use Strict Directive legal in `function f() { 'use strict'; }`.
A rule that additionally needs there to be *something* to be simple about must ask that separately.
Whether the `arguments` object aliases a parameter is such a rule: with no parameters there is
nothing to alias, but the parameter list is simple all the same.
"""
return all(isinstance(param, JsIdentifier) for param in fn.params)
def has_parameter_expressions(fn: JsFunctionNode) -> bool:
"""
Whether *fn*'s parameter list holds an expression that runs when the function is called
(`ContainsExpression`, §8.6.2): a default anywhere in it, or a computed key of an object
pattern. A rest element is descended into, since a pattern inside one may hold either.
This is what decides whether a function has a parameter scope of its own. A parameter list with
no expression in it cannot observe the difference: nothing in it runs, so nothing in it can read
a name, and the body may as well hold the parameters. One with an expression can, and the
expression evaluates before the body's declarations exist.
The answer must be exact rather than merely safe in one direction. Answering `False` where an
expression stands leaves a default reading what the body declares; answering `True` where none
does splits one binding into two, which costs every consumer that reads a parameter and its
body together the reference the other half records.
"""
return any(_contains_expression(param) for param in fn.params)
def _contains_expression(node: Node | None) -> bool:
if node is None or isinstance(node, JsIdentifier):
return False
if isinstance(node, JsAssignmentPattern):
return True
if isinstance(node, JsRestElement):
return _contains_expression(node.argument)
if isinstance(node, JsArrayPattern):
return any(_contains_expression(element) for element in node.elements)
if isinstance(node, JsObjectPattern):
return any(
isinstance(prop, JsRestElement) and _contains_expression(prop.argument)
or isinstance(prop, JsProperty) and (prop.computed or _contains_expression(prop.value))
for prop in node.properties
)
return True
class ParameterGrammar(enum.Enum):
"""
The grammar a function's parameter list is read through, which decides how many parameters it may
hold and whether a name may repeat among them. It is a fact about the *position* the function
stands in rather than about the function: `function (a, a) {}` is a program as the value of a
property and a Syntax Error as a method, and the two are the same node.
"""
#: `FormalParameters`. A repeated name is legal, and only sloppy mode and a simple list keep it so.
FORMAL = enum.auto()
#: `UniqueFormalParameters`. A repeated name is a Syntax Error in either mode.
UNIQUE = enum.auto()
#: A getter, which takes no parameters at all.
GETTER = enum.auto()
#: `PropertySetParameterList`. Exactly one parameter, and never a rest element.
SETTER = enum.auto()
_PROPERTY_ACCESSORS = {
JsPropertyKind.GET: ParameterGrammar.GETTER,
JsPropertyKind.SET: ParameterGrammar.SETTER,
}
_METHOD_ACCESSORS = {
JsMethodKind.GET: ParameterGrammar.GETTER,
JsMethodKind.SET: ParameterGrammar.SETTER,
}
def parameter_grammar(fn: JsFunctionNode) -> ParameterGrammar:
"""
Which grammar *fn* takes its parameters through. An arrow always takes `UniqueFormalParameters`;
a method, a getter and a setter take theirs through the member that holds them, so the member is
what is asked. A function standing anywhere else — including as the plain value of a property,
which is the shape a method is easily confused with — takes `FormalParameters`.
"""
if isinstance(fn, JsArrowFunctionExpression):
return ParameterGrammar.UNIQUE
parent = fn.parent
if isinstance(parent, JsProperty) and parent.value is fn and parent.method:
return _PROPERTY_ACCESSORS.get(parent.kind, ParameterGrammar.UNIQUE)
if isinstance(parent, JsMethodDefinition) and parent.value is fn:
return _METHOD_ACCESSORS.get(parent.kind, ParameterGrammar.UNIQUE)
return ParameterGrammar.FORMAL
_STRICT_RESERVED = frozenset({
'implements',
'interface',
'let',
'package',
'private',
'protected',
'public',
'static',
'yield',
})
_EVAL_ARGS = frozenset({'eval', 'arguments'})
def _child_strictness(node: Node, strict: bool) -> bool:
if isinstance(node, JsScript):
return strict or node.module or declares_use_strict(node)
if isinstance(node, (JsClassDeclaration, JsClassExpression)):
return True
if not isinstance(node, FUNCTION_NODES):
return strict
body = node.body
if isinstance(body, JsBlockStatement):
return strict or declares_use_strict(body)
return strict
def _reserved_by_own_kind(fn: JsFunctionNode) -> frozenset[str]:
"""
The names *fn* reserves by being the kind of function it is: a generator reserves `yield` and an
async function reserves `await`, because inside one the word is an operator and cannot also name
anything.
"""
names: set[str] = set()
if isinstance(fn, (JsFunctionDeclaration, JsFunctionExpression)) and fn.generator:
names.add('yield')
if fn.is_async:
names.add('await')
return frozenset(names)
def reserved_by_function_kind(node: Node) -> frozenset[str]:
"""
The names that may name nothing at *node*, because of the kind of function whose code *node* is.
Unlike the strict-mode reserved words this holds in either mode: `function* g(yield) {}` and
`async function h(await) {}` are texts no engine reads, sloppy file or not.
The region a function reserves for is its own parameter list and its own body, and it stops at
every function written inside it — `function* g() { function h(yield) {} }` is a program, because
`h`'s code is `h`'s and not the generator's. An arrow is the exception in half: its parameters are
still the enclosing function's code and inherit the reservation, while its body is its own and
does not, so `(yield) => {}` inside a generator is refused and `() => { var yield = 1; }` is not.
A function's name is governed by one context and never by two, but which one depends on how the
function is written. A declaration's name is bound outside it and takes the enclosing context, so
`function* yield() {}` is read at the top level and refused inside a generator; naming itself
therefore skips this function's own reservation and keeps climbing. An expression's name is bound
inside it and takes its own kind alone (§15.2.1, §15.5.1, §15.8.1), so
`x = (function* yield() {})` is refused while `function* g() { var f = function yield() {}; }` is
read; naming itself therefore answers here and does not climb at all.
"""
reserved: set[str] = set()
cursor: Node = node
parent = cursor.parent
while parent is not None:
if isinstance(parent, FUNCTION_NODES):
if isinstance(parent, JsFunctionExpression) and cursor is parent.id:
return _reserved_by_own_kind(parent)
names_itself = isinstance(parent, JsFunctionDeclaration) and cursor is parent.id
if not names_itself:
reserved |= _reserved_by_own_kind(parent)
inherits = (
isinstance(parent, JsArrowFunctionExpression)
and any(cursor is param for param in parent.params)
)
if not inherits:
break
cursor, parent = parent, parent.parent
return frozenset(reserved)
_KIND_RESERVABLE = frozenset({'yield', 'await'})
"""
Every name any function kind reserves, which is what `_reserved_by_own_kind` can ever answer with. A
name outside this set is reserved by no kind, so asking which kinds enclose it cannot change the
answer — and that question is a climb to the nearest enclosing function, asked once per identifier in
the tree.
"""
def _check_kind_reserved(node: Node, out: list[StrictViolation]) -> None:
if not isinstance(node, JsIdentifier) or node.name not in _KIND_RESERVABLE:
return
if names_a_property(node):
return
if node.name in reserved_by_function_kind(node):
out.append(StrictViolation(node.offset, 'reserved-by-function-kind', node.name))
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):
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 _flag_name(ident: JsIdentifier, out: list[StrictViolation]) -> None:
"""
Report *ident* where strict code refuses the name it carries. The name is the one its escapes
denote, which is what ECMA-262 states these rules over: a StringValue is asked for, not the
text that spelled it.
V8 asks the text at two of the positions this reaches. `function f(ev\\u0061l) {}` and
`argum\\u0065nts = 1` are accepted there under strict where `function f(eval) {}` and
`arguments = 1` are refused, though `var ev\\u0061l = 1` is refused like its plain spelling.
Reporting all of them is the specification's answer and the deliberate divergence: what reads
this decides whether a text may be spliced somewhere, so agreeing with the specification costs
a splice nobody writes and disagreeing with it would pass one no other engine runs.
"""
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 _suspend_operator_in_parameters(fn: JsFunctionNode) -> Node | None:
"""
A `yield` or `await` operator written into *fn*'s parameter list, or `None` where none is. No
parameter list may hold either, in any kind of function and in either mode: a default value is
evaluated as the call is being entered, before there is anything to suspend.
A function written inside a parameter list is not descended into. Its parameters are its own, and
the walk reaches it in its own right; its body is a place where the operator can be perfectly
legal, `function f(a = function* () { yield 1; }) {}` being a program.
The first such operator in source order is the one answered with, which is the order
`collect_strict_violations` reports in; a stack visits what is pushed last first, so each run of
children is pushed reversed.
"""
stack: list[Node] = list(reversed(fn.params))
while stack:
node = stack.pop()
if isinstance(node, (JsYieldExpression, JsAwaitExpression)):
return node
if isinstance(node, FUNCTION_NODES):
continue
stack.extend(reversed(node.children()))
return None
def _check_function(
fn: JsFunctionNode,
strict: bool,
out: list[StrictViolation],
handled: set[int],
) -> None:
"""
Every early error a function's signature carries. *strict* is the mode the function's own code
runs in, which its body may have declared; it decides the name rules and one of the three clauses
that forbid a repeated parameter. The other two, the arity of an accessor, and the directive rule
hold whatever mode the program is in.
A Use Strict Directive is illegal under a parameter list that is not simple (§15.2.1). The rule is
the body's own prologue against the list, and not the mode: in a body that is already strict the
directive changes nothing and the text is refused all the same, which is the shape a promoted
directive lands in.
A repeated name is a Syntax Error under three separate clauses, and reporting on any one of them
alone is wrong in one direction or the other. `UniqueFormalParameters` — every arrow and every
method definition — forbids it outright; `FormalParameters` forbids it in strict code, and forbids
it in either mode once the list holds anything that is not a plain identifier.
"""
grammar = parameter_grammar(fn)
simple = has_simple_parameters(fn)
suspend = _suspend_operator_in_parameters(fn)
if suspend is not None:
out.append(StrictViolation(suspend.offset, 'suspend-in-parameters'))
if not simple and declares_use_strict(fn.body):
out.append(StrictViolation(fn.offset, 'use-strict-with-non-simple-parameters'))
if grammar is ParameterGrammar.GETTER and fn.params:
out.append(StrictViolation(fn.offset, 'accessor-arity'))
if grammar is ParameterGrammar.SETTER and (
len(fn.params) != 1
or isinstance(fn.params[0], JsRestElement)
):
out.append(StrictViolation(fn.offset, 'accessor-arity'))
repeats_are_errors = strict or not simple or grammar is not ParameterGrammar.FORMAL
seen: set[str] = set()
for param in fn.params:
for ident in _target_identifiers(param):
handled.add(id(ident))
if strict:
_flag_name(ident, out)
if ident.name not in seen:
seen.add(ident.name)
elif repeats_are_errors:
out.append(StrictViolation(ident.offset, 'duplicate-parameter', 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_function(node, child_strict, out, handled)
_flag_bound(node.id, child_strict, out, handled)
elif isinstance(node, JsArrowFunctionExpression):
_check_function(node, 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 names_a_property(node)
):
out.append(StrictViolation(node.offset, 'reserved-word', node.name))
def collect_strict_violations(node: Node, *, strict: bool = False) -> list[StrictViolation]:
"""
Every 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.
Not every rule asks about the mode. A Use Strict Directive under a parameter list that is not
simple, a repeated name where the grammar requires a unique list, the arity of an accessor, and a
name a generator or an async function reserves are refused whatever mode the program runs in, so a
sloppy seed can report on a tree with no `"use strict"` anywhere in it. That is what makes a sloppy
seed a usable gate on text about to be spliced into a destination whose mode is not yet known.
An empty result means the tree has no parse error under the seeded mode; 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_kind_reserved(current, 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(node)-
Whether a string literal was written with an escape that strict code rejects. It is the same spelling a template excludes from its grammar, so the scan itself lives beside the escapes it reads and both rules ask it there.
Expand source code Browse git
def has_octal_string_escape(node: JsStringLiteral) -> bool: """ Whether a string literal was written with an escape that strict code rejects. It is the same spelling a template excludes from its grammar, so the scan itself lives beside the escapes it reads and both rules ask it there. """ return has_legacy_numeric_escape(node.body) def is_use_strict(node)-
Whether a literal spells the Use Strict Directive. It is asked of the spelling rather than of the value, because a directive is one: a literal that denotes
use strictthrough an escape is not the directive, and neither is one the source never closed.Expand source code Browse git
def is_use_strict(node: JsStringLiteral) -> bool: """ Whether a literal spells the Use Strict Directive. It is asked of the spelling rather than of the value, because a directive is one: a literal that denotes `use strict` through an escape is not the directive, and neither is one the source never closed. """ return node.terminated and node.body == 'use strict' def spelling_states(body)-
What a literal's spelling states, as against what it denotes: whether it is the Use Strict Directive, and whether it carries an escape strict code rejects. Both are facts about how the literal was written and about nothing else, so a pass that re-spells one may do so only where neither answer moves — re-spelling
'use\x20strict'as'use strict'writes a directive the source never wrote, and every line behind it becomes strict code.Expand source code Browse git
def spelling_states(body: str) -> tuple[bool, bool]: """ What a literal's spelling states, as against what it denotes: whether it is the Use Strict Directive, and whether it carries an escape strict code rejects. Both are facts about how the literal was written and about nothing else, so a pass that re-spells one may do so only where neither answer moves — re-spelling `'use\\x20strict'` as `'use strict'` writes a directive the source never wrote, and every line behind it becomes strict code. """ return body == 'use strict', has_legacy_numeric_escape(body) def statement_list(node)-
The statement list node holds directly, or
Nonewhen it holds none. Only the three node types that can host a Directive Prologue are answered for, so a caller that already knows it is looking at a host reads the list here without consulting the tree above it.Expand source code Browse git
def statement_list(node: Node | None) -> list[Statement] | None: """ The statement list *node* holds directly, or `None` when it holds none. Only the three node types that can host a Directive Prologue are answered for, so a caller that already knows it is looking at a host reads the list here without consulting the tree above it. """ if isinstance(node, (JsScript, JsBlockStatement, JsStaticBlock)): return node.body return None def is_prologue_host(node)-
Whether node holds a statement list that a Directive Prologue can open (§11.2.1): a script body, a function body, or a class static block. Nothing else does. A plain block, the body of a
try,catchorfinally, a labelled statement, aswitchcase and the expression body of a concise arrow all hold code no directive governs, so a'use strict'written at the head of one is an ordinary string-valued statement that changes no mode.A function body is recognized through the function that owns it, because a body and a plain block are the same node type and only the tree above tells them apart.
Expand source code Browse git
def is_prologue_host(node: Node | None) -> bool: """ Whether *node* holds a statement list that a Directive Prologue can open (§11.2.1): a script body, a function body, or a class static block. Nothing else does. A plain block, the body of a `try`, `catch` or `finally`, a labelled statement, a `switch` case and the expression body of a concise arrow all hold code no directive governs, so a `'use strict'` written at the head of one is an ordinary string-valued statement that changes no mode. A function body is recognized through the function that owns it, because a body and a plain block are the same node type and only the tree above tells them apart. """ if isinstance(node, (JsScript, JsStaticBlock)): return True if isinstance(node, JsBlockStatement): owner = node.parent return isinstance(owner, FUNCTION_NODES) and owner.body is node return False def directive_prologue(host)-
The Directive Prologue of host: the run of statements it opens with that consist of nothing but a string literal. The run ends at the first statement that is anything else, so it is a prefix, and every statement behind that one is ordinary code however it happens to be spelled.
A parenthesized literal is not one of them. A directive is a statement whose expression is the literal, so
('use strict');states nothing, and the parser keeps the parenthesis as a node of its own precisely so that this stays decidable.host is taken to be a prologue host; where a caller must find the host from a statement inside it,
is_prologue_host()decides that.Expand source code Browse git
def directive_prologue(host: Node | None) -> list[JsExpressionStatement]: """ The Directive Prologue of *host*: the run of statements it opens with that consist of nothing but a string literal. The run ends at the first statement that is anything else, so it is a prefix, and every statement behind that one is ordinary code however it happens to be spelled. A parenthesized literal is not one of them. A directive is a statement whose expression *is* the literal, so `('use strict');` states nothing, and the parser keeps the parenthesis as a node of its own precisely so that this stays decidable. *host* is taken to be a prologue host; where a caller must find the host from a statement inside it, `is_prologue_host` decides that. """ return leading_string_statements(statement_list(host) or []) def leading_string_statements(statements)-
The opening run of statements that consist of nothing but a string literal. Where statements is a prologue host's own list this is its Directive Prologue; a caller holding the list rather than the host — the printer, which is handed a body — asks here.
Expand source code Browse git
def leading_string_statements(statements: list[Statement]) -> list[JsExpressionStatement]: """ The opening run of *statements* that consist of nothing but a string literal. Where *statements* is a prologue host's own list this is its Directive Prologue; a caller holding the list rather than the host — the printer, which is handed a body — asks here. """ run: list[JsExpressionStatement] = [] for statement in statements: if not isinstance(statement, JsExpressionStatement): break if not isinstance(statement.expression, JsStringLiteral): break run.append(statement) return run def declares_use_strict(host)-
Whether the Directive Prologue of host holds the Use Strict Directive, which makes the code host encloses strict. The directive need not open the prologue: every string-literal statement ahead of it is a directive too, and one the language does not recognize is simply inert.
It is the same directive
is_use_strict_directive()names, which is what keeps the mode this reports and the mode the printer writes from parting company. A string an edit lifted to the head of a body declares nothing — the printer puts it in a bracket precisely so that it cannot — and a body read as strict on the strength of one would be reasoned about in a mode the text will not have.Expand source code Browse git
def declares_use_strict(host: Node | None) -> bool: """ Whether the Directive Prologue of *host* holds the Use Strict Directive, which makes the code *host* encloses strict. The directive need not open the prologue: every string-literal statement ahead of it is a directive too, and one the language does not recognize is simply inert. It is the same directive `is_use_strict_directive` names, which is what keeps the mode this reports and the mode the printer writes from parting company. A string an edit lifted to the head of a body declares nothing — the printer puts it in a bracket precisely so that it cannot — and a body read as strict on the strength of one would be reasoned about in a mode the text will not have. """ return any(is_use_strict_directive(statement) for statement in directive_prologue(host)) def mark_directives(root)-
Record on every statement of every Directive Prologue in the tree at root that the source wrote it where a directive stands. A parser calls this once over the finished tree, which is the only point at which the question can be answered: a statement is a directive by virtue of the list it sits in and the statements ahead of it, and neither is known while it is being built.
The mark is provenance and never a conclusion. Whether a marked statement still is a directive is asked of where it stands now; what the mark adds is the other half, that a statement standing at the head of a body today was not merely put there by an edit.
Expand source code Browse git
def mark_directives(root: Node) -> None: """ Record on every statement of every Directive Prologue in the tree at *root* that the source wrote it where a directive stands. A parser calls this once over the finished tree, which is the only point at which the question can be answered: a statement is a directive by virtue of the list it sits in and the statements ahead of it, and neither is known while it is being built. The mark is provenance and never a conclusion. Whether a marked statement still *is* a directive is asked of where it stands now; what the mark adds is the other half, that a statement standing at the head of a body today was not merely put there by an edit. """ for node in root.walk(): if is_prologue_host(node): for statement in directive_prologue(node): statement.directive = True def names_module_syntax(root)-
Whether the tree at root holds syntax only module code may hold: an
importorexportdeclaration, orimport.meta. A dynamicimport()is not among them — it is available to a script as well — and neither is a top-levelawait, which the parser reads as a name followed by a call and which would therefore fire on any program that happens to useawaitas an ordinary identifier.The answer is a lower bound and never a refutation. §16.1 leaves module-ness to the host, so a module that spells none of this syntax is one nothing in the text distinguishes from a script.
Expand source code Browse git
def names_module_syntax(root: Node) -> bool: """ Whether the tree at *root* holds syntax only module code may hold: an `import` or `export` declaration, or `import.meta`. A dynamic `import()` is not among them — it is available to a script as well — and neither is a top-level `await`, which the parser reads as a name followed by a call and which would therefore fire on any program that happens to use `await` as an ordinary identifier. The answer is a lower bound and never a refutation. §16.1 leaves module-ness to the host, so a module that spells none of this syntax is one nothing in the text distinguishes from a script. """ for node in root.walk(): if isinstance(node, ( JsImportDeclaration, JsExportAllDeclaration, JsExportDefaultDeclaration, JsExportNamedDeclaration, )): return True if isinstance(node, JsMetaProperty) and node.meta == 'import' and node.property == 'meta': return True return False def mark_module(script)-
Record on script whether its source is module code, as
names_module_syntax()observes it. A parser calls this once over the finished tree, besidemark_directives(): both record what the source was, at the one point where the whole source is in hand and no edit has moved anything yet.Expand source code Browse git
def mark_module(script: JsScript) -> None: """ Record on *script* whether its source is module code, as `names_module_syntax` observes it. A parser calls this once over the finished tree, beside `mark_directives`: both record what the source was, at the one point where the whole source is in hand and no edit has moved anything yet. """ script.module = names_module_syntax(script) def is_use_strict_directive(statement)-
Whether statement is the Use Strict Directive, and so the reason some body runs in strict mode. Three things must hold and each rules out a different mistake.
The source must have written it where a directive stands, or an edit that moved a string here is credited with a mode the file never declared. It must still stand in a prologue host's opening run, or a directive carried somewhere else by a clone or a splice is credited with a mode it no longer declares. And it must spell
use strict, because every other directive is inert and deleting one changes nothing.This is the predicate a removal asks before dropping a statement and an insertion asks before stepping over one, so that the two cannot disagree about which statement is at stake.
Expand source code Browse git
def is_use_strict_directive(statement: Statement) -> bool: """ Whether *statement* is the Use Strict Directive, and so the reason some body runs in strict mode. Three things must hold and each rules out a different mistake. The source must have written it where a directive stands, or an edit that moved a string here is credited with a mode the file never declared. It must still stand in a prologue host's opening run, or a directive carried somewhere else by a clone or a splice is credited with a mode it no longer declares. And it must spell `use strict`, because every other directive is inert and deleting one changes nothing. This is the predicate a removal asks before dropping a statement and an insertion asks before stepping over one, so that the two cannot disagree about which statement is at stake. """ if not isinstance(statement, JsExpressionStatement) or not statement.directive: return False expression = statement.expression if not isinstance(expression, JsStringLiteral) or not is_use_strict(expression): return False host = statement.parent if not is_prologue_host(host): return False return any(member is statement for member in directive_prologue(host)) def keeping_directives(host, replacement)-
replacement with a Use Strict Directive that host currently opens with put back at its head, for a caller about to install replacement as host's whole body. Nothing is carried where replacement already opens with one, whether that is the host's own statement or a copy of it: a pass that rebuilds a body by cloning it keeps the directive by value and not by identity, and carrying the original as well would write the directive twice.
A directive is what a body opens with, so one the caller kept but put behind another statement declares nothing where it now stands and the host's is put in front of it. It is moved there and never copied ahead of itself: a tree holds a node in one place, and a list naming the same statement at two indices would leave one node with one parent standing at both, which every
id-keyed map over the tree then reads as two. Where the statement stood it computed a string and discarded it, so taking it from there changes nothing but the mode it now declares.Whole-body replacement is the one way a directive is lost without a removal: nothing is deleted, the statement is simply absent from the list handed in, so a rule phrased over removals cannot see it. It is repaired rather than refused because a pass reaches this point having already rewritten what it is about to install, and declining here would leave those rewrites standing over a body that never received them.
Expand source code Browse git
def keeping_directives(host: Node, replacement: list[Statement]) -> list[Statement]: """ *replacement* with a Use Strict Directive that *host* currently opens with put back at its head, for a caller about to install *replacement* as *host*'s whole body. Nothing is carried where *replacement* already opens with one, whether that is the host's own statement or a copy of it: a pass that rebuilds a body by cloning it keeps the directive by value and not by identity, and carrying the original as well would write the directive twice. A directive is what a body opens with, so one the caller kept but put behind another statement declares nothing where it now stands and the host's is put in front of it. It is *moved* there and never copied ahead of itself: a tree holds a node in one place, and a list naming the same statement at two indices would leave one node with one parent standing at both, which every `id`-keyed map over the tree then reads as two. Where the statement stood it computed a string and discarded it, so taking it from there changes nothing but the mode it now declares. Whole-body replacement is the one way a directive is lost without a removal: nothing is deleted, the statement is simply absent from the list handed in, so a rule phrased over removals cannot see it. It is repaired rather than refused because a pass reaches this point having already rewritten what it is about to install, and declining here would leave those rewrites standing over a body that never received them. """ if not is_prologue_host(host) or _opens_with_use_strict(replacement): return replacement carried = next( ( statement for statement in directive_prologue(host) if is_use_strict_directive(statement) ), None, ) if carried is None: return replacement return [carried, *(kept for kept in replacement if kept is not carried)] def promoted_use_strict(statements)-
Which statements of statements would be read as the Use Strict Directive without ever having been written as one. statements is a prologue host's own list, so its opening run of string-literal statements is a Directive Prologue: a member of that run spelling
use strictand carrying no mark came to stand there through an edit, and writing it plain makes the body strict where the source left it sloppy.None are reported once the run holds a directive the source did write. The body is strict either way, so there is no mode to save, and a parenthesis there would end the run and eject every real directive standing behind it — which is how a repair becomes a second defect.
Only
use strictis reported. Every other promoted string is inert wherever it lands: it declares no mode, and parenthesizing it would end the run for nothing.Expand source code Browse git
def promoted_use_strict(statements: list[Statement]) -> list[JsExpressionStatement]: """ Which statements of *statements* would be read as the Use Strict Directive without ever having been written as one. *statements* is a prologue host's own list, so its opening run of string-literal statements is a Directive Prologue: a member of that run spelling `use strict` and carrying no mark came to stand there through an edit, and writing it plain makes the body strict where the source left it sloppy. None are reported once the run holds a directive the source did write. The body is strict either way, so there is no mode to save, and a parenthesis there would end the run and eject every real directive standing behind it — which is how a repair becomes a second defect. Only `use strict` is reported. Every other promoted string is inert wherever it lands: it declares no mode, and parenthesizing it would end the run for nothing. """ promoted: list[JsExpressionStatement] = [] for statement in leading_string_statements(statements): expression = statement.expression if not isinstance(expression, JsStringLiteral) or not is_use_strict(expression): continue if statement.directive: return [] promoted.append(statement) return promoted def joins_directive_prologue(statement)-
Whether statement would enter the Directive Prologue of the body that holds it were it spelled as a string literal: it sits in a prologue host, and nothing but string-literal statements precede it. A pass that rewrites such a statement into a literal hands the prologue that statement and every string-literal statement standing behind it, so a
'use strict'that was ordinary code becomes the directive that makes the whole body strict.Expand source code Browse git
def joins_directive_prologue(statement: Statement) -> bool: """ Whether *statement* would enter the Directive Prologue of the body that holds it were it spelled as a string literal: it sits in a prologue host, and nothing but string-literal statements precede it. A pass that rewrites such a statement into a literal hands the prologue that statement *and* every string-literal statement standing behind it, so a `'use strict'` that was ordinary code becomes the directive that makes the whole body strict. """ host = statement.parent body = statement_list(host) if body is None or not is_prologue_host(host): return False index = len(directive_prologue(host)) return index < len(body) and body[index] is statement def strict_mode_at(node)-
Whether the code at node runs in strict mode. Mode is inherited (§11.2.2): a body is strict when its own Directive Prologue declares it or when the code enclosing it is strict, and every part of a class definition is strict whatever encloses it (§15.7). node itself counts, so asking this of a function body answers the mode that body runs in.
A function's directive reaches further than the body that holds it: the parameter list and the name the function binds are strict code too, which is why
function f(eval) { 'use strict'; }is refused andfunction f(eval) {}is a program. Neither stands inside the body, so the whole function is asked, not only the host.Module code is strict throughout (§11.2.2), whatever any body in it declares, so the climb ends by asking the script it arrives at. What it asks is
mark_module()'s observation of the source, which only ever reports a module and never denies one: a program the host loads as a module while its text names no import, export orimport.metais read here as a script, and the mode it is given is the weaker of the two.Expand source code Browse git
def strict_mode_at(node: Node) -> bool: """ Whether the code at *node* runs in strict mode. Mode is inherited (§11.2.2): a body is strict when its own Directive Prologue declares it or when the code enclosing it is strict, and every part of a class definition is strict whatever encloses it (§15.7). *node* itself counts, so asking this of a function body answers the mode that body runs in. A function's directive reaches further than the body that holds it: the parameter list and the name the function binds are strict code too, which is why `function f(eval) { 'use strict'; }` is refused and `function f(eval) {}` is a program. Neither stands inside the body, so the whole function is asked, not only the host. Module code is strict throughout (§11.2.2), whatever any body in it declares, so the climb ends by asking the script it arrives at. What it asks is `mark_module`'s observation of the source, which only ever reports a module and never denies one: a program the host loads as a module while its text names no import, export or `import.meta` is read here as a script, and the mode it is given is the weaker of the two. """ cursor: Node | None = node while cursor is not None: if isinstance(cursor, JsScript) and cursor.module: return True if isinstance(cursor, (JsClassDeclaration, JsClassExpression)): return True if is_prologue_host(cursor) and declares_use_strict(cursor): return True if isinstance(cursor, FUNCTION_NODES) and declares_use_strict(cursor.body): return True cursor = cursor.parent return False def has_simple_parameters(fn)-
Whether fn has a simple parameter list (§15.1.3): every parameter is a plain identifier, with no default, no rest element and no destructuring. An empty list is simple — nothing in it is anything else — which is what makes a Use Strict Directive legal in
function f() { 'use strict'; }.A rule that additionally needs there to be something to be simple about must ask that separately. Whether the
argumentsobject aliases a parameter is such a rule: with no parameters there is nothing to alias, but the parameter list is simple all the same.Expand source code Browse git
def has_simple_parameters(fn: JsFunctionNode) -> bool: """ Whether *fn* has a simple parameter list (§15.1.3): every parameter is a plain identifier, with no default, no rest element and no destructuring. An empty list is simple — nothing in it is anything else — which is what makes a Use Strict Directive legal in `function f() { 'use strict'; }`. A rule that additionally needs there to be *something* to be simple about must ask that separately. Whether the `arguments` object aliases a parameter is such a rule: with no parameters there is nothing to alias, but the parameter list is simple all the same. """ return all(isinstance(param, JsIdentifier) for param in fn.params) def has_parameter_expressions(fn)-
Whether fn's parameter list holds an expression that runs when the function is called (
ContainsExpression, §8.6.2): a default anywhere in it, or a computed key of an object pattern. A rest element is descended into, since a pattern inside one may hold either.This is what decides whether a function has a parameter scope of its own. A parameter list with no expression in it cannot observe the difference: nothing in it runs, so nothing in it can read a name, and the body may as well hold the parameters. One with an expression can, and the expression evaluates before the body's declarations exist.
The answer must be exact rather than merely safe in one direction. Answering
Falsewhere an expression stands leaves a default reading what the body declares; answeringTruewhere none does splits one binding into two, which costs every consumer that reads a parameter and its body together the reference the other half records.Expand source code Browse git
def has_parameter_expressions(fn: JsFunctionNode) -> bool: """ Whether *fn*'s parameter list holds an expression that runs when the function is called (`ContainsExpression`, §8.6.2): a default anywhere in it, or a computed key of an object pattern. A rest element is descended into, since a pattern inside one may hold either. This is what decides whether a function has a parameter scope of its own. A parameter list with no expression in it cannot observe the difference: nothing in it runs, so nothing in it can read a name, and the body may as well hold the parameters. One with an expression can, and the expression evaluates before the body's declarations exist. The answer must be exact rather than merely safe in one direction. Answering `False` where an expression stands leaves a default reading what the body declares; answering `True` where none does splits one binding into two, which costs every consumer that reads a parameter and its body together the reference the other half records. """ return any(_contains_expression(param) for param in fn.params) def parameter_grammar(fn)-
Which grammar fn takes its parameters through. An arrow always takes
UniqueFormalParameters; a method, a getter and a setter take theirs through the member that holds them, so the member is what is asked. A function standing anywhere else — including as the plain value of a property, which is the shape a method is easily confused with — takesFormalParameters.Expand source code Browse git
def parameter_grammar(fn: JsFunctionNode) -> ParameterGrammar: """ Which grammar *fn* takes its parameters through. An arrow always takes `UniqueFormalParameters`; a method, a getter and a setter take theirs through the member that holds them, so the member is what is asked. A function standing anywhere else — including as the plain value of a property, which is the shape a method is easily confused with — takes `FormalParameters`. """ if isinstance(fn, JsArrowFunctionExpression): return ParameterGrammar.UNIQUE parent = fn.parent if isinstance(parent, JsProperty) and parent.value is fn and parent.method: return _PROPERTY_ACCESSORS.get(parent.kind, ParameterGrammar.UNIQUE) if isinstance(parent, JsMethodDefinition) and parent.value is fn: return _METHOD_ACCESSORS.get(parent.kind, ParameterGrammar.UNIQUE) return ParameterGrammar.FORMAL def reserved_by_function_kind(node)-
The names that may name nothing at node, because of the kind of function whose code node is. Unlike the strict-mode reserved words this holds in either mode:
function* g(yield) {}andasync function h(await) {}are texts no engine reads, sloppy file or not.The region a function reserves for is its own parameter list and its own body, and it stops at every function written inside it —
function* g() { function h(yield) {} }is a program, becauseh's code ish's and not the generator's. An arrow is the exception in half: its parameters are still the enclosing function's code and inherit the reservation, while its body is its own and does not, so(yield) => {}inside a generator is refused and() => { var yield = 1; }is not.A function's name is governed by one context and never by two, but which one depends on how the function is written. A declaration's name is bound outside it and takes the enclosing context, so
function* yield() {}is read at the top level and refused inside a generator; naming itself therefore skips this function's own reservation and keeps climbing. An expression's name is bound inside it and takes its own kind alone (§15.2.1, §15.5.1, §15.8.1), sox = (function* yield() {})is refused whilefunction* g() { var f = function yield() {}; }is read; naming itself therefore answers here and does not climb at all.Expand source code Browse git
def reserved_by_function_kind(node: Node) -> frozenset[str]: """ The names that may name nothing at *node*, because of the kind of function whose code *node* is. Unlike the strict-mode reserved words this holds in either mode: `function* g(yield) {}` and `async function h(await) {}` are texts no engine reads, sloppy file or not. The region a function reserves for is its own parameter list and its own body, and it stops at every function written inside it — `function* g() { function h(yield) {} }` is a program, because `h`'s code is `h`'s and not the generator's. An arrow is the exception in half: its parameters are still the enclosing function's code and inherit the reservation, while its body is its own and does not, so `(yield) => {}` inside a generator is refused and `() => { var yield = 1; }` is not. A function's name is governed by one context and never by two, but which one depends on how the function is written. A declaration's name is bound outside it and takes the enclosing context, so `function* yield() {}` is read at the top level and refused inside a generator; naming itself therefore skips this function's own reservation and keeps climbing. An expression's name is bound inside it and takes its own kind alone (§15.2.1, §15.5.1, §15.8.1), so `x = (function* yield() {})` is refused while `function* g() { var f = function yield() {}; }` is read; naming itself therefore answers here and does not climb at all. """ reserved: set[str] = set() cursor: Node = node parent = cursor.parent while parent is not None: if isinstance(parent, FUNCTION_NODES): if isinstance(parent, JsFunctionExpression) and cursor is parent.id: return _reserved_by_own_kind(parent) names_itself = isinstance(parent, JsFunctionDeclaration) and cursor is parent.id if not names_itself: reserved |= _reserved_by_own_kind(parent) inherits = ( isinstance(parent, JsArrowFunctionExpression) and any(cursor is param for param in parent.params) ) if not inherits: break cursor, parent = parent, parent.parent return frozenset(reserved) def collect_strict_violations(node, *, strict=False)-
Every 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.Not every rule asks about the mode. A Use Strict Directive under a parameter list that is not simple, a repeated name where the grammar requires a unique list, the arity of an accessor, and a name a generator or an async function reserves are refused whatever mode the program runs in, so a sloppy seed can report on a tree with no
"use strict"anywhere in it. That is what makes a sloppy seed a usable gate on text about to be spliced into a destination whose mode is not yet known.An empty result means the tree has no parse error under the seeded mode; 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 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. Not every rule asks about the mode. A Use Strict Directive under a parameter list that is not simple, a repeated name where the grammar requires a unique list, the arity of an accessor, and a name a generator or an async function reserves are refused whatever mode the program runs in, so a sloppy seed can report on a tree with no `"use strict"` anywhere in it. That is what makes a sloppy seed a usable gate on text about to be spliced into a destination whose mode is not yet known. An empty result means the tree has no parse error under the seeded mode; 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_kind_reserved(current, 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 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. The parse tree is never changed.Most rules record that the code at
offsetwould be aSyntaxErrorif its enclosing region ran in strict mode. Four do not: a Use Strict Directive under a parameter list that is not simple, a repeated name in a list the grammar requires to be unique, the arity of an accessor, and a name reserved by the kind of function it stands in are refused in either mode, so a caller that treats an empty result as "sloppy code is safe" is reading it right, and one that treats a non-empty result as "only strict code would refuse this" is not.Expand source code Browse git
@dataclass(frozen=True) class StrictViolation: """ A single 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. The parse tree is never changed. Most rules record that the code at `offset` would be a `SyntaxError` if its enclosing region ran in strict mode. Four do not: a Use Strict Directive under a parameter list that is not simple, a repeated name in a list the grammar requires to be unique, the arity of an accessor, and a name reserved by the kind of function it stands in are refused in *either* mode, so a caller that treats an empty result as "sloppy code is safe" is reading it right, and one that treats a non-empty result as "only strict code would refuse this" is not. """ 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.
class ParameterGrammar (*args, **kwds)-
The grammar a function's parameter list is read through, which decides how many parameters it may hold and whether a name may repeat among them. It is a fact about the position the function stands in rather than about the function:
function (a, a) {}is a program as the value of a property and a Syntax Error as a method, and the two are the same node.Expand source code Browse git
class ParameterGrammar(enum.Enum): """ The grammar a function's parameter list is read through, which decides how many parameters it may hold and whether a name may repeat among them. It is a fact about the *position* the function stands in rather than about the function: `function (a, a) {}` is a program as the value of a property and a Syntax Error as a method, and the two are the same node. """ #: `FormalParameters`. A repeated name is legal, and only sloppy mode and a simple list keep it so. FORMAL = enum.auto() #: `UniqueFormalParameters`. A repeated name is a Syntax Error in either mode. UNIQUE = enum.auto() #: A getter, which takes no parameters at all. GETTER = enum.auto() #: `PropertySetParameterList`. Exactly one parameter, and never a rest element. SETTER = enum.auto()Ancestors
- enum.Enum
Class variables
var FORMAL-
FormalParameters. A repeated name is legal, and only sloppy mode and a simple list keep it so. var UNIQUE-
UniqueFormalParameters. A repeated name is a Syntax Error in either mode. var GETTER-
A getter, which takes no parameters at all.
var SETTER-
PropertySetParameterList. Exactly one parameter, and never a rest element.