Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
281f15e
Bound plain JavaScript literal parser failures to their native values
yashrajp22 Nov 5, 2026
7b456db
Keep malformed ordinary literals on the conservative fallback
yashrajp22 Nov 5, 2026
045971c
Preserve JavaScript fast path and existing runtime diagnostics
yashrajp22 Nov 5, 2026
6a75f9b
Bound JavaScript regex candidates by native literal ranges
yashrajp22 Nov 5, 2026
b51d274
Keep unresolved execution outside JavaScript regex boundary proof
yashrajp22 Nov 5, 2026
d9bf4ea
Align regex controls with existing bounded parser contracts
yashrajp22 Nov 5, 2026
b1561af
Require bounded primitive String receivers for regex ownership
yashrajp22 Nov 5, 2026
ec9ae75
Retain conservative regex ownership inside dynamic with scopes
yashrajp22 Nov 5, 2026
9ec7e25
Limit JavaScript boundary refinement to proved native regex cases
yashrajp22 Nov 5, 2026
00b5f9d
Preserve the JavaScript no-regex execution fast path
yashrajp22 Nov 5, 2026
9c63f32
Keep proven matching patterns outside shell command reconstruction
yashrajp22 Nov 5, 2026
c6efeef
Merge branch 'main' into yashraj/javascript-string-boundaries-followup
github-actions[bot] Nov 6, 2026
121b70c
Merge branch 'main' into yashraj/javascript-string-boundaries-followup
github-actions[bot] Nov 6, 2026
19d29af
Merge branch 'main' into yashraj/javascript-string-boundaries-followup
github-actions[bot] Nov 6, 2026
50dca1f
Merge branch 'main' into yashraj/javascript-string-boundaries-followup
github-actions[bot] Nov 6, 2026
29cdf55
Merge branch 'main' into yashraj/javascript-string-boundaries-followup
github-actions[bot] Nov 6, 2026
0d27530
Merge main and retain native regular expression boundaries
yashrajp22 Nov 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 165 additions & 1 deletion src/skillspector/javascript_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -1499,6 +1499,7 @@ def javascript_template_spans(
check_runtime: Callable[[], None],
*,
unresolved_executables: list[int] | None = None,
regex_literals: list[tuple[int, int]] | None = None,
) -> JavaScriptTemplateSpans | None:
"""Prove host delimiters while retaining unresolved executable templates.

Expand All @@ -1514,11 +1515,19 @@ def javascript_template_spans(
if unresolved_executables is not None:
unresolved_executables.append(content.index("`"))
return None
executable = None
if regex_literals is not None and any(node.type == "regex" for node in program.nodes):
# A lexical boundary cannot replace unresolved behavior. Apply the
# existing execution barrier even when the source has no templates.
executable = _has_shell_execution_call(program, check_runtime)
if not executable:
regex_literals.extend(_plain_regex_value_spans(program, check_runtime))
templates = [node for node in program.nodes if node.type == "template_string"]
if not templates:
return None
projected = assertion_template_shell_text(content, check_runtime)
executable = _has_shell_execution_call(program, check_runtime)
if executable is None:
executable = _has_shell_execution_call(program, check_runtime)
if executable:
for node in templates:
check_runtime()
Expand Down Expand Up @@ -1561,6 +1570,161 @@ def javascript_template_spans(
return tuple(starts), tuple(ends)


def _plain_regex_value_spans(
program: JavaScriptProgram, check_runtime: Callable[[], None]
) -> list[tuple[int, int]]:
"""Bound lexical regex candidates after the execution barrier.

Only direct String values and immutable top-level String bindings supply
native matching receivers. Explicit overrides retain the shell fallback.
"""
matching = {"match", "matchAll", "search", "split", "replace", "replaceAll"}
references: dict[str, list[Node]] = {}
candidates: dict[str, tuple[Node, Node]] = {}
overrides = {
"String",
"RegExp",
"Reflect",
"prototype",
"constructor",
"__proto__",
"defineProperty",
"defineProperties",
"getPrototypeOf",
"setPrototypeOf",
"__defineGetter__",
"__defineSetter__",
}
for node in program.nodes:
check_runtime()
if node.type == "with_statement":
return []
if node.type in {
"identifier",
"shorthand_property_identifier",
"shorthand_property_identifier_pattern",
}:
references.setdefault(program.text(node), []).append(node)
if node.type == "variable_declarator":
binding = node.child_by_field_name("name")
value = node.child_by_field_name("value")
declaration = node.parent
if (
binding is not None
and binding.type == "identifier"
and value is not None
and value.type == "string"
and declaration is not None
and declaration.type == "lexical_declaration"
and declaration.children[0].type == "const"
and declaration.parent == program.root_node
):
candidates[program.text(binding)] = (binding, node)
if node.type in {"identifier", "property_identifier", "string"} and (
program.text(node).strip("\"'") in overrides
):
return []
if node.type in {"method_definition", "pair"}:
name = node.child_by_field_name("name") or node.child_by_field_name("key")
if name is not None and (
name.type == "computed_property_name" or program.text(name).strip("\"'") in matching
):
return []
if node.type in {"assignment_expression", "augmented_assignment_expression"}:
target = node.child_by_field_name("left")
if target is not None and (
target.type == "subscript_expression"
or target.type == "member_expression"
and _callable_member(program, target) in matching
):
return []
for node in program.nodes:
check_runtime()
property_node = node.child_by_field_name("property") or node.child_by_field_name("index")
if node.type in {"member_expression", "subscript_expression"} and (
_callable_member(program, node) == "source"
or property_node is not None
and "\\" in program.text(property_node)
or node.type == "subscript_expression"
and (property_node is None or property_node.type not in {"string", "number"})
):
# Source extraction needs a separate use proof before a pattern
# can stop lending boundaries to executable text.
return []
string_references: set[int] = set()
budget = _MAX_TOKENS * 4
for name, (binding, declaration) in candidates.items():
owned = {binding.id}
for reference in references[name]:
budget -= 1
if budget < 0:
return []
check_runtime()
if reference == binding:
continue
member = reference.parent
call = None if member is None else member.parent
if not (
reference.type == "identifier"
and reference.start_byte >= declaration.end_byte
and member is not None
and member.type == "member_expression"
and member.child_by_field_name("object") == reference
and _callable_member(program, member) in matching
and call is not None
and call.type == "call_expression"
and call.child_by_field_name("function") == member
):
break
owned.add(reference.id)
else:
string_references.update(owned)
spans: list[tuple[int, int]] = []
for node in program.nodes:
check_runtime()
if node.type != "regex":
continue
pattern = node.child_by_field_name("pattern")
flags = node.child_by_field_name("flags")
flag_text = program.text(flags) if flags is not None else ""
if (
pattern is None
or len(set(flag_text)) != len(flag_text)
or set(flag_text) - set("dgimsuvy")
or {"u", "v"} <= set(flag_text)
):
continue
start, end = program.span(node)
pattern_end = program.span(pattern)[1]
if (
program.content[start : start + 1] != "/"
or program.content[pattern_end : pattern_end + 1] != "/"
):
continue
arguments = node.parent
call = None if arguments is None else arguments.parent
member = None if call is None else call.child_by_field_name("function")
receiver = None if member is None else member.child_by_field_name("object")
previous = node.prev_named_sibling
while previous is not None and previous.type == "comment":
check_runtime()
previous = previous.prev_named_sibling
if (
arguments is not None
and arguments.type == "arguments"
and previous is None
and call is not None
and call.type == "call_expression"
and member is not None
and member.type == "member_expression"
and _callable_member(program, member) in matching
and receiver is not None
and (receiver.type == "string" or receiver.id in string_references)
):
spans.append((start, end))
return spans


def _scalar_template_identifiers(
program: JavaScriptProgram, check_runtime: Callable[[], None]
) -> set[int]:
Expand Down
21 changes: 20 additions & 1 deletion src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -3403,6 +3403,7 @@ def _has_shell_command_word_exhaustion(
quoted_printf_allowed = _quoted_printf_allowed and not _shell_redefines_command(
content, check_runtime
)

parsed_through = 0
# A word that spans lines may pair a heredoc, comment or prose quote with
# a later command line. Main skips the candidates inside such a word, so
Expand Down Expand Up @@ -3447,7 +3448,11 @@ def _has_shell_command_word_exhaustion(
continue
if start in owned_word_positions:
continue
if javascript_source is not None and javascript_source.owns(start):
if javascript_source is not None and (
javascript_source.owns(start) or javascript_source.regex_literal_end(start) is not None
):
# Proven native matching patterns have no shell-word semantics.
# This candidate owns no later host code or raw finding input.
continue
if powershell is not None and powershell.owns(start):
continue
Expand Down Expand Up @@ -6134,14 +6139,18 @@ def __init__(self, content: str, check_runtime: Callable[[], None]) -> None:
self._spans: JavaScriptTemplateSpans | None = None
self._spans_computed = False
self._unresolved_executables: list[int] = []
self._regex_literals: list[tuple[int, int]] = []
self._regex_starts: list[int] = []

def _owned_spans(self) -> JavaScriptTemplateSpans | None:
if not self._spans_computed:
self._spans = _javascript_template_spans(
self._content,
self._check_runtime,
unresolved_executables=self._unresolved_executables,
regex_literals=self._regex_literals,
)
self._regex_starts = [start for start, _ in self._regex_literals]
self._spans_computed = True
return self._spans

Expand All @@ -6158,6 +6167,16 @@ def unresolved_execution_diagnostic(self) -> StaticParserDiagnostic | None:
ParserUncheckedScope.REMAINING_INPUT,
)

def regex_literal_end(self, position: int) -> int | None:
"""Bound a matching-language candidate by its complete native literal."""
self._owned_spans()
index = bisect_right(self._regex_starts, position) - 1
if index >= 0:
start, end = self._regex_literals[index]
if start <= position < end:
return end
return None

def owns(self, position: int) -> bool:
spans = self._owned_spans()
if spans is None:
Expand Down
Loading
Loading