Skip to content

Unified: Add folder-based fallback for static name resolution - #22370

Merged
asgerf merged 13 commits into
github:mainfrom
asgerf:unified/folder-fallback
Aug 21, 2026
Merged

Unified: Add folder-based fallback for static name resolution#22370
asgerf merged 13 commits into
github:mainfrom
asgerf:unified/folder-fallback

Conversation

@asgerf

@asgerf asgerf commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Static name resolution relies on Package.swift manifests to indicate how files are linked up. However, it seems only about 50% of real-world projects use such manifest files. In the absence of a known manifest file, we had no way of linking up names across files.

This PR adds a generic fallback mechanism for files that are not covered by a known manifest. An unqualified name X, appearing in a file without a known manifest, may now resolve to any (exported) top-level declaration X. If multiple such declarations exist, we prioritise the one "closest" in the file hierarchy, meaning the one with the lowest common ancestor in the folder tree.

If two definitions are tied for being closest, none is resolved as the target. Some more real-world examples of this would help motivate what to do here.

Evaluation

The PR also adds meta and metric queries for measuring the success rate of static name binding (first commit). I wanted to make sure the meta queries and metric query stay in sync, so they now share code through AnalysisQuality.qll.

Note that the percentage of statically resolvable names is currently not very useful because 100% is not an attainable number due to dependencies not being extracted. A lower number is not necessarily bad, because MaD models can still be evaluated by stepping through the name-binding graph directly. With that said, resolution rate, averaged over all projects, increases from 31% to 37%. An increase is always good, though it doesn't count as a "success rate".

See the DCA report for more concrete stats.

@asgerf
asgerf force-pushed the unified/folder-fallback branch from 01d2ec1 to 60dd3ba Compare August 18, 2026 08:10
@asgerf asgerf added the no-change-note-required This PR does not need a change note label Aug 18, 2026
@asgerf
asgerf force-pushed the unified/folder-fallback branch 4 times, most recently from 26940cf to 252d8cd Compare August 20, 2026 07:46
asgerf added 6 commits August 20, 2026 13:22
The spurious result is offered by the folder-based heuristic, but would have been blocked by proper shadowing support.
Inheritance and access to nested classes is one of the reasons not to apply the heuristic as a "fixup" after the recursive layer, because when a base class is resolved through the folder heuristic, access to nested members still need to be resolved through the standard logic.
@asgerf
asgerf force-pushed the unified/folder-fallback branch from 252d8cd to b6741aa Compare August 20, 2026 11:23
}

module EntityReportStats<EntityStatsSig Input> {
private import Input
Comment thread unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql Fixed
Comment thread unified/ql/src/diagnostic/StaticNameResolution.ql Fixed

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

Adds folder-based fallback name resolution for Swift files without package manifests, plus analysis-quality telemetry.

Changes:

  • Adds folder-proximity-based static name binding.
  • Adds resolution and manifest-coverage diagnostics.
  • Adds tests for unique and ambiguous cross-file names.
Show a summary per file
File Description
unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll Implements folder-based name resolution.
unified/ql/lib/codeql/unified/internal/AnalysisQuality.qll Defines reusable quality statistics.
shared/util/codeql/util/ReportStats.qll Adds entity-based statistics reporting.
unified/ql/src/diagnostic/StaticNameResolution.ql Reports resolved static names.
unified/ql/src/diagnostic/FilesCoveredByModuleManifest.ql Reports manifest-covered files.
unified/ql/src/diagnostic/ExtractorInformation.ql Exposes statistics as telemetry.
unified/ql/test/library-tests/static-name-binding/unqualified-access.swift Records a heuristic false positive.
unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Use.swift Tests sibling-file resolution and ambiguity.
unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Def1.swift Adds sibling declarations.
unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/Def2.swift Adds competing sibling declarations.
unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder1/Def.swift Adds nested declarations.
unified/ql/test/library-tests/static-name-binding/not-a-package/SiblingFiles/SubFolder2/Def.swift Adds competing nested declarations.
unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Runner.swift Tests proximity in the Main tree.
unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Util/Util.swift Tests nested Main resolution.
unified/ql/test/library-tests/static-name-binding/not-a-package/Main/Drivers/Driver.swift Defines Main-specific targets.
unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Runner.swift Tests proximity in the Mock tree.
unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Util/Util.swift Tests nested Mock resolution.
unified/ql/test/library-tests/static-name-binding/not-a-package/Mock/Drivers/Driver.swift Defines Mock-specific targets.

Review details

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 18/18 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll Outdated
Comment thread unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll
@asgerf

asgerf commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

@asgerf
asgerf marked this pull request as ready for review August 20, 2026 14:20
@asgerf
asgerf requested review from a team as code owners August 20, 2026 14:20
@asgerf
asgerf requested a review from hvitved August 20, 2026 14:20

@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.

LGTM, a few questions.

// currently blocked on getting static name binding to report this information.
}

NameBindingNode getTarget() {

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.

Ultimately, I guess we will want to expose this in the facade AST?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Something like this I guess, though I'm still not sure if the name-binding node should be kept internal to the name-binding passes, or if we should interface with it downstream.

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.

Yes, I think we will want to keep NameBindingNode internal (like ItemNode in Rust).

}

/**
* Holds if `file` has a one of the definitions of the given ambiguous 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.

grammar

* Holds if `folder` has two or more subfolders containing a definition of `name`.
*/
private predicate hasConflictingDefs(Folder folder, string name) {
containsDef(folder, name) and

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.

Is this line needed for performance?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, this was not actually needed anymore, after the second line was added. Removing the redundant line.

result = folder
or
result = getOutermostNonConflictingScope(folder.getParentContainer(), name) and
not isOutermostNonConflictingScope(folder, name) and

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.

Is this line not implied?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, removing

private module FolderHeuristic {
private predicate topLevelNameDef(File file, string name, NameBindingNode node) {
exists(TopLevel top, Stmt stmt, NameDeclaration nameDecl |
top.getFile() = file and

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.

Should there be a

not top.getFile() = any(ModuleScopeRepr r).getAnIncludedFile()

restriction?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's deliberate. The heuristic currently allows stores from all files, but reads going into module-less files only.

It makes no difference for the repos I tested on, but the behaviour is less surprising IMO when we reason about names being uniquely defined in a certain folder.

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.

Ok, as long as this was deliberate 👍

@asgerf
asgerf merged commit 5a69ef0 into github:main Aug 21, 2026
109 of 111 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants