Skip to content

Unified: implement local scoping#22120

Merged
tausbn merged 13 commits into
github:mainfrom
asgerf:unified/local-scoping
Jul 8, 2026
Merged

Unified: implement local scoping#22120
tausbn merged 13 commits into
github:mainfrom
asgerf:unified/local-scoping

Conversation

@asgerf

@asgerf asgerf commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

This adds local scoping and does a few other things in order to get that working:

  • Moves libraries into codeql.unified.internal and imports their Public modules into unified.qll. Makes it easy to share internal code between the libraries.
  • The last commit sets out to use Yeast context-passing to correctly translate identifers in patterns to either name_pattern or expr_equality_pattern wrapping a name_expr. In the future we might want to move this logic into QL but I wanted to make sure that Yeast can handle this kind of problem if we need it to. The commit also fixes a bunch of potential context-leaking issues where modifiers would leak into expr/stmt subtrees.

guard statements could not be natively handled by the shared library, but it turns out can handle it by flattening their children into the enclosing Block.

@asgerf asgerf added the no-change-note-required This PR does not need a change note label Jul 3, 2026
@github-actions github-actions Bot added the Ruby label Jul 3, 2026
Comment on lines +17 to +19
/**
* Declaration of a local or top-level variable.
*/
Comment on lines +29 to +31
/**
* Declaration of a local or top-level function.
*/
// TODO: self
}

predicate accessCand(AstNode n, string name) {
*/
predicate relevantNode(AstNode node) {
// Match an ancestor node by location so its whole subtree is shown.
node.getParent*().getLocation().toString().matches("%test.swift@227:%")
@asgerf asgerf requested a review from Copilot July 3, 2026 12:18
@asgerf asgerf marked this pull request as ready for review July 3, 2026 12:19
@asgerf asgerf requested review from a team as code owners July 3, 2026 12:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces lexical (local) scoping support to the Unified libraries by adding a name-binding–based Variables library, restructuring Unified’s public surface to re-export internal Public modules via unified.qll, and updating the Swift Yeast translation to correctly distinguish binding patterns from expression-equality patterns while preventing modifier/context leakage into unrelated subtrees.

Changes:

  • Add a Unified local-variable model (Variables) backed by codeql/namebinding, plus a developer debug query for the scope graph.
  • Update Swift extractor translation to use context-passing + context resets so patterns are translated correctly and binding modifiers don’t leak into initializers/bodies.
  • Add/expand Swift corpus and Unified library-tests covering variable binding, shadowing, and guard/conditional scoping behaviors.
Show a summary per file
File Description
unified/ql/test/library-tests/variables/variables.ql Adds an InlineExpectationsTest query to validate VariableAccessVariable resolution via inline comments.
unified/ql/test/library-tests/variables/variables.expected Inline-expectations .expected placeholder for the new test (expected to be empty).
unified/ql/test/library-tests/variables/test.swift Adds a comprehensive Swift fixture exercising shadowing and scoping scenarios.
unified/ql/lib/unified.qll Switches Unified’s public imports to re-export internal Public modules (Variables/Ast extras).
unified/ql/lib/qlpack.yml Adds dependency on codeql/namebinding to support local name binding.
unified/ql/lib/codeql/unified/internal/Variables.qll Implements local variable declarations/accesses and scope lookup wiring for Unified.
unified/ql/lib/codeql/unified/internal/dev/debugScopeGraph.ql Adds a dev-only graph query to render/debug the local scope graph.
unified/ql/lib/codeql/unified/internal/AstExtra.qll Adds non-generated AST helper classes (including Comment) under an internal module.
unified/ql/lib/codeql/unified/Comments.qll Removes the old comments helper (moved under internal AstExtra).
unified/extractor/tests/corpus/swift/variables/tuple-destructuring-binding.output Updates expected AST output for tuple destructuring bindings.
unified/extractor/tests/corpus/swift/variables/binding-modifier-does-not-leak-into-initializer.swift Adds corpus fixture ensuring binding modifiers don’t leak into initializer translation.
unified/extractor/tests/corpus/swift/variables/binding-modifier-does-not-leak-into-initializer.output Adds expected output for the initializer leakage fixture.
unified/extractor/tests/corpus/swift/types/binding-modifier-does-not-leak-into-accessor-body.swift Adds corpus fixture ensuring binding modifiers don’t leak into accessor bodies.
unified/extractor/tests/corpus/swift/types/binding-modifier-does-not-leak-into-accessor-body.output Adds expected output for the accessor-body leakage fixture.
unified/extractor/tests/corpus/swift/control-flow/binding-modifier-does-not-leak-to-sibling.swift Adds corpus fixture ensuring binding modifiers don’t leak to sibling statements.
unified/extractor/tests/corpus/swift/control-flow/binding-modifier-does-not-leak-to-sibling.output Adds expected output for the sibling leakage fixture.
unified/extractor/src/languages/swift/swift.rs Updates Swift Yeast rules to publish binding context, translate/reset subtrees, and pattern-translate identifiers appropriately.
shared/yeast/src/build.rs Adds translate_reset helper to translate captures under a fresh user context.
shared/namebinding/codeql/namebinding/LocalNameBinding.qll Adjusts the namebinding interface and adds a debug scope graph helper module.
ruby/ql/lib/codeql/ruby/ast/internal/Variable.qll Updates Ruby implementation to match the adjusted namebinding signature.

Review details

  • Files reviewed: 19/20 changed files
  • Comments generated: 7
  • Review effort level: Low

Comment thread shared/yeast/src/build.rs Outdated
Comment thread unified/ql/lib/codeql/unified/internal/Variables.qll Outdated
Comment thread unified/ql/lib/codeql/unified/internal/AstExtra.qll Outdated
Comment thread unified/ql/test/library-tests/variables/test.swift Outdated
Comment thread unified/ql/test/library-tests/variables/test.swift Outdated
Comment thread unified/ql/test/library-tests/variables/test.swift Outdated
Comment thread unified/ql/test/library-tests/variables/test.swift Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

@hvitved hvitved left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QL changes LGTM, I mostly have some minor comments. I have approved in case we want to merge in order to unblock other work; my comments can be addressed follow-up if needed.

private class LogicalAndRoot extends LogicalAndExpr {
LogicalAndRoot() { not this = any(LogicalAndExpr e).getAnOperand() }

private Expr getDescendent(string path) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: getDescendant.

)
}

Expr getNthLeaf(int n) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do something similar in Rust for let chains; perhaps we should consider making this transformation inside the shared library.

predicate accessCand(AstNode n, string name) {
n.(NameExpr).getIdentifier().getValue() = name
or
n.(NamePattern).getIdentifier().getValue() = name

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why this case is needed; I realize that removing it removes the results on lines 347 and 348 in test.swift, but those look like declarations and not accesses to me.

n.(Identifier).getValue() = name
}

predicate lookupStartsAt(AstNode n, AstNode scope) { none() }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove?

/**
* A local variable.
*/
class Variable extends LocalNameBindingOutput::Local {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to exclude local functions from this class at some point (same for VariableAccess).

// Switch with multiple cases
func t11(value: Int) { // name=value1
switch value { // $ access=value1
case let x where x > 0: // name=x1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also expect an access=x1 here.

let x = 2 // name=x2
print(x) // $ access=x2
}
inner() // $ access=inner1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't know if it is valid in Swift, but it would be nice to have:

let inner = 2 // name=inner2
print(inner) // $access=inner2

after this line.

}
}

// Switch with same variable name in different cases

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment does not match test case

/** Holds if `node` should be included in the debug tree. */
private signature predicate relevantNodeSig(AstNode node);

module DebugScopeGraph<relevantNodeSig/1 relevantNode> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Comment on lines +244 to +247
definingNode = getEnclosingOrPattern(pattern)
or
not exists(getEnclosingOrPattern(pattern)) and
definingNode = pattern

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, something similar is done for Rust, so there may be potential for sharing.

@tausbn tausbn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the changes here look good. My only concern is the new translate_reset mechanism, which is a bit too "all or nothing" for my taste. I've come up with a slightly different API that I think is equally good, but a bit more flexible. In the interest of moving things along, I'll approve and merge this PR, and then make the necessary improvements later on.

@tausbn tausbn merged commit 3c3f740 into github:main Jul 8, 2026
137 of 142 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-change-note-required This PR does not need a change note Ruby

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants