Skip to content

fix(extraction): reach a Swift URL built by a constructor - #1976

Open
CaptainMittens wants to merge 5 commits into
DeusData:mainfrom
CaptainMittens:fix/swift-nested-url-constructor
Open

fix(extraction): reach a Swift URL built by a constructor#1976
CaptainMittens wants to merge 5 commits into
DeusData:mainfrom
CaptainMittens:fix/swift-nested-url-constructor

Conversation

@CaptainMittens

@CaptainMittens CaptainMittens commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Swift has no URL literal, so real code almost never passes a bare string to a request. It writes URL(string: "https://…")!, and the literal then sits two levels below the argument list — past the trailing !, which the grammar models as a postfix_expression, and inside the constructor's own value_arguments.

extract_url_or_topic_arg saw only the outer node and gave up, so the URL never reached the service-pattern table and no Route node formed. That is the shape #1892 reported from a real project.

This PR stacks on #1896 and #1986

GitHub cannot base a cross-fork pull request on a branch in the fork, so this PR shows five commits. Only the second is this PR's own work:

Commit Belongs to
2fd719d8 reach Swift call arguments through call_suffix #1896 — approved, please review there
68cc0658 reach a Swift URL built by a constructor this PR
93ebe29e stop the Swift scanner shifting past the width of an int #1986 — please review there
8f00939d stop the Swift try-bang entry shifting past its own type #1986
82e1de8c record the Swift scanner shift patch in MANIFEST.md #1986

#1896 is the layer underneath: Swift models a call as a target plus a call_suffix, so args was null for every Swift call and no URL ever arrived. Reaching a bare string argument was only half the problem — this PR reaches the constructor that wraps it. Merge #1896 and #1986 first and this one narrows to its single commit.

The fix

swift_unwrap_url_constructor() steps past both wrappers. It unwraps only URL, URLComponents and URLRequest, so any other constructor keeps its own meaning and the outer call does not borrow the inner call's string. A non-literal argument such as URL(string: base + path) falls through to the ordinary handling unchanged.

The value_argument unwrap added for the bare-string case became swift_argument_value(), because the nested argument list needs the same step and the code was identical.

The vendored grammar fix, corrected

An earlier version of this description said the carried grammar commit was #1977's and would merge without conflict because it was byte-identical. That is no longer true and the branch has been rebuilt. #1977 is closed; #1986 supersedes it and is strictly larger — it fixes eat_operators as #1977 did and OP_SYMBOL_SUPPRESSOR (1UL <<1ULL <<), which matters because unsigned long is 32 bits under LLP64, so that one is undefined on exactly the Windows CLANGARM64 leg.

This branch now carries #1986's three commits in full, including the MANIFEST.md row, rather than #1977's single commit. The vendored diff on this branch and on #1986 are byte-identical again — verified by diffing origin/main...HEAD for swift/scanner.c, MANIFEST.md and scripts/vendored-checksums.txt across both branches — so this PR shrinks on its own when #1986 lands instead of needing a rebase.

This PR's test is the first thing in the tree to reach the Swift scanner's suppressor path — the rule that stops try! emitting its ! as its own token, which a force-unwrapped URL(...)! hits. Keeping the fix on this branch is what stops that test reaching a release build without it.

Tests

Three in tests/test_extraction.c, one of them a negative control:

Test Covers
swift_nested_url_constructor_issue1892 URL(string: "…")! — the force-unwrapped shape from the report
swift_nested_url_no_bang_issue1892 URLRequest(url: "…") — no !, so no postfix_expression wrapper
swift_non_url_constructor_untouched_issue1892 Formatter(pattern: "%s-%d") must yield no string, so the unwrap cannot over-reach

Plus a pipeline test in tests/test_pipeline.c that the Route node now forms.

Refs #1892

@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@DeusData

DeusData commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Reviewed. The extraction change is good and I have nothing to ask of it.

swift_unwrap_url_constructor unwraps an explicit three-name allowlist rather than any constructor, every step is null-guarded and returns the node unchanged on the way out, and URL(string: base + path) falls through to ordinary handling. swift_non_url_constructor_untouched_issue1892 is the test that makes that claim checkable rather than stated — a negative control on an unwrap is exactly the right instinct, because the failure mode of this kind of fix is silently borrowing an inner call's string.

Pulling the value_argument unwrap out as swift_argument_value() once the nested list needed the same step is the right call too.

Three things about the stack, so you can sequence it without guessing.

1. #1896 should go first, and it can go on its own. It is the base layer, it is self-contained, and — unlike this one — it touches no vendored file, so it has no manifest obligation. Once it merges, this PR narrows to its own commit and gets much easier to read.

2. This PR needs a MANIFEST.md row, same as #1986. It changes internal/cbm/vendored/grammars/swift/scanner.c and updates scripts/vendored-checksums.txt, but not internal/cbm/vendored/grammars/MANIFEST.md. Any change to vendored sources has to be recorded in that file's "Local source patches" table — the vendoring table claims our copy matches the pinned upstream commit, and a local patch makes that false unless it is written down, so a future re-vendor would silently drop the fix. The suggested row is on #1986.

3. #1977 is closed — #1986 supersedes it, and that changes one of your assumptions here. #1986 is by you and strictly larger: it fixes eat_operators as #1977 did and OP_SYMBOL_SUPPRESSOR (1UL <<1ULL <<), which matters because unsigned long is 32-bit under LLP64, so that one is undefined on exactly the Windows leg you identified. Full reasoning is on #1977.

The consequence for this branch: this PR's description says the carried grammar commit is byte-identical to the standalone one and will therefore merge without conflict. That was true of #1977 but is not true of #1986 — the superset has a hunk yours does not. So if #1986 lands first, expect this branch to need a rebase rather than to shrink on its own. Worth knowing before it surprises you.

One unrelated heads-up: main does not currently compile — two changes landed the same type short-name index into type_registry.h and git merged them into duplicate members. Your green here predates that, so it is stale rather than wrong; checks will need a re-run once main is fixed. #1993 has the repair. Nothing for you to do.

Thanks for splitting the grammar fix out into its own PR rather than leaving it buried in this one, and for laying the stack out in the description — the commit-to-PR table made this quick to follow.

@DeusData DeusData added bug Something isn't working parsing/quality Graph extraction bugs, false positives, missing edges dependencies Pull requests that update a dependency file priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. labels Sep 1, 2026
handle_calls reads a call's arguments through one tree-sitter field
lookup, ts_node_child_by_field_name(node, "arguments"). The vendored
Swift grammar declares no "arguments" field at all -- its own
ts_field_names[] table has zero occurrences, against one in Go and two
in TypeScript. Swift models a call as a target expression plus a
call_suffix, and the arguments hang off the suffix as value_arguments.

So args was null for every Swift call ever parsed, first_string_arg was
never populated, and no Swift HTTP call could raise an HTTP_CALLS edge
or a Route node. Alamofire, Moya and URLSession have been in the
service-pattern table the whole time and match the callee text
correctly; the URL simply never arrived.

Three changes, all in extract_calls.c:

- swift_call_args() reaches the argument list through call_suffix, in
  the same shape as the existing objectscript_call_args() fallback and
  used from the same place. A trailing closure has no value_arguments,
  so it returns a null node and that call behaves as before.
- extract_url_or_topic_arg() unwraps Swift's per-argument
  value_argument node, stepping past a leading value_argument_label.
  Without it, dataTask(with: "/api/v1/widgets") yields the label "with"
  rather than the path. PHP and C# already had the same unwrap for
  their own "argument" node.
- is_string_like() gains line_string_literal, which is what Swift calls
  an ordinary "..." literal. The list already held raw_string_literal,
  so only Swift's common case was missing.

This is the layer underneath DeusData#1892 rather than the whole of it. A
literal URL argument now arrives; a literal nested inside a
constructor, as in URLSession.shared.data(from: URL(string: "...")!),
still does not, because extract_positional_url reads a literal, a
template string, a concatenation or a named constant and that shape is
none of them.

Reproduce-first: all three tests fail without the fix, two on a null
first_string_arg and one on HTTP_CALLS being 0.

Fixes DeusData#1892

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
Swift has no URL literal, so almost no real code passes a bare string to a
request. It writes URL(string: "https://…")! instead, and the literal then
sits two levels below the argument list: past the trailing "!", which the
grammar models as a postfix_expression, and inside the constructor's own
value_arguments.

extract_url_or_topic_arg saw only the outer node and gave up, so the URL
never reached the service-pattern table and no Route node formed. That is
the shape issue DeusData#1892 reported from a real project — reaching a bare string
argument was only the layer underneath it.

swift_unwrap_url_constructor() steps past both wrappers. It unwraps only
URL, URLComponents and URLRequest, so any other constructor keeps its own
meaning and the outer call does not borrow the inner call's string. A
non-literal argument such as URL(string: base + path) falls through to the
ordinary handling unchanged.

The value_argument unwrap added for the bare-string case is now
swift_argument_value(), because the nested argument list needs the same
step and the code was identical.

Refs DeusData#1892

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
The Swift scanner keeps a 64-bit mask of the symbols that suppress a
match -- the rule that stops `try!` emitting its `!` as a token of its
own. It tests one bit per candidate:

    uint64_t suppressing_symbols = OP_SYMBOL_SUPPRESSOR[full_match];
    for (uint64_t suppressor = 0; suppressor < TOKEN_COUNT; suppressor++) {
        if (!(suppressing_symbols & 1 << suppressor)) {

The mask is uint64_t but the literal `1` is an int, so the shift is an
int shift. TOKEN_COUNT is larger than 32, so once suppressor reaches 31
the shift runs past the width of the type. That is undefined behavior,
and every bit above 31 is tested against a value the standard does not
define.

Nothing caught it because nothing in the tree reached the suppressor
path. Any Swift force-unwrap does: `cached!` is enough.

UBSan reports it as:

    scanner.c:514:47: runtime error: left shift of 1 by 31 places
    cannot be represented in type 'int'

`1ULL` makes the literal as wide as the mask it is tested against.

The new test in tests/test_extraction.c cannot go red on its own. The
normal test build prints the UBSan message and carries on, which is why
this survived. The Windows CLANGARM64 leg runs UBSan in trap mode, and
there the same shift is an illegal-instruction crash -- so the test
exists to make sure that leg keeps parsing a force-unwrap at all.

scripts/vendored-checksums.txt records the new hash for the one changed
file, as scripts/security-vendored.sh --update writes it. Layer 8 of the
security gate compares vendored content against that manifest, so the
edit and its recorded hash belong in the same commit.

Found while adding Swift URL extraction in DeusData#1892 / DeusData#1976, and split out
of that PR so the vendored change can be reviewed on its own.

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
scanner.c line 131 shifts a literal 1UL by 32 to build the entry that
suppresses the bang of a try!. unsigned long is 64 bits on Linux and
macOS but 32 bits on Windows, so there the shift count equals the width
of the type, which is undefined.

1ULL is 64 bits on every target this project builds for.

Nothing reports it today. The expression is a compile-time constant, so
no sanitizer sees it run, and Makefile.cbm:719 builds vendored grammars
with -w, which switches off -Wshift-count-overflow.

Upstream tree-sitter-swift made the same change in 6ab8d1d74ebd, after
the commit this grammar is pinned to.

Refs DeusData#1892

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
The vendoring table claims our copy of a grammar matches the pinned
upstream commit. A local patch makes that claim false unless the patch
is written down, so a future re-vendor would drop the fix without
anyone noticing.

The row covers both changed lines, because both are the same defect in
the same file: OP_SYMBOL_SUPPRESSOR at line 131 and eat_operators at
line 514.

MANIFEST.md is itself listed in scripts/vendored-checksums.txt, so its
own recorded checksum moves with the edit.

Refs DeusData#1892

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
CaptainMittens added a commit to CaptainMittens/codebase-memory-mcp that referenced this pull request Sep 1, 2026
The Swift scanner keeps a 64-bit mask of the symbols that suppress a
match -- the rule that stops `try!` emitting its `!` as a token of its
own. It tests one bit per candidate:

    uint64_t suppressing_symbols = OP_SYMBOL_SUPPRESSOR[full_match];
    for (uint64_t suppressor = 0; suppressor < TOKEN_COUNT; suppressor++) {
        if (!(suppressing_symbols & 1 << suppressor)) {

The mask is uint64_t but the literal `1` is an int, so the shift is an
int shift. TOKEN_COUNT is larger than 32, so once suppressor reaches 31
the shift runs past the width of the type. That is undefined behavior,
and every bit above 31 is tested against a value the standard does not
define.

Nothing caught it because nothing in the tree reached the suppressor
path. Any Swift force-unwrap does: `cached!` is enough.

UBSan reports it as:

    scanner.c:514:47: runtime error: left shift of 1 by 31 places
    cannot be represented in type 'int'

`1ULL` makes the literal as wide as the mask it is tested against.

The new test in tests/test_extraction.c cannot go red on its own. The
normal test build prints the UBSan message and carries on, which is why
this survived. The Windows CLANGARM64 leg runs UBSan in trap mode, and
there the same shift is an illegal-instruction crash -- so the test
exists to make sure that leg keeps parsing a force-unwrap at all.

scripts/vendored-checksums.txt records the new hash for the one changed
file, as scripts/security-vendored.sh --update writes it. Layer 8 of the
security gate compares vendored content against that manifest, so the
edit and its recorded hash belong in the same commit.

Found while adding Swift URL extraction in DeusData#1892 / DeusData#1976, and split out
of that PR so the vendored change can be reviewed on its own.

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
@CaptainMittens
CaptainMittens force-pushed the fix/swift-nested-url-constructor branch from 458f9fc to 82e1de8 Compare September 1, 2026 22:04
@CaptainMittens

Copy link
Copy Markdown
Contributor Author

Manifest row added, branch rebuilt on current main, and the description corrected. Thank you for the #1977#1986 catch — it changed what the right fix here was.

The stale assumption, and what I did with it

You were right that "byte-identical, so it merges without conflict" was true of #1977 and false of #1986. Rather than delete the claim, I made it true again: this branch now carries #1986's three commits in fulleat_operators, OP_SYMBOL_SUPPRESSOR, and the MANIFEST.md row — instead of #1977's single commit.

I checked that rather than assuming it. Diffing origin/main...HEAD for swift/scanner.c, MANIFEST.md and scripts/vendored-checksums.txt across this branch and #1986 returns nothing — the vendored change is identical on both. So this PR shrinks on its own when #1986 lands, which is what the description now says.

That also answers your manifest point: the row arrives with #1986's commit rather than as a separate edit here, so the two branches cannot drift into two different rows for one patch.

Current stack

Commit Belongs to
2fd719d8 reach Swift call arguments through call_suffix #1896
68cc0658 reach a Swift URL built by a constructor this PR
93ebe29e + 8f00939d + 82e1de8c the scanner shift fix and its manifest row #1986

Merge order as you set it: #1896, then #1986, then this one narrows to a single commit.

Verified on the rebuilt branch with -fsanitize=address,undefined: test-runner extraction pipeline grammar_regression598 passed, no failures and no UBSan runtime reports. All four Swift tests pass, including swift_force_unwrap_scanner_shift which is the one that reaches the suppressor path. bash scripts/security-vendored.sh passes too.

On the coupling

Taken, and it was fair. Five open PRs across two stacks with shared files is how a stale assumption survived into a description in the first place — I could not hold all of it, so I stopped re-reading the parts I thought I already knew.

I am opening nothing further on these stacks until #1896 and #1986 are in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working dependencies Pull requests that update a dependency file parsing/quality Graph extraction bugs, false positives, missing edges priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants