From 1925c2058293310ebd32d71e995dc399ea822219 Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Wed, 2 Sep 2026 19:40:51 +0200 Subject: [PATCH 1/3] Rust: Make crate fallback logic more conservative in path resolution library --- .../codeql/rust/internal/PathResolution.qll | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/rust/ql/lib/codeql/rust/internal/PathResolution.qll b/rust/ql/lib/codeql/rust/internal/PathResolution.qll index bd9322487161..a79908ac6f00 100644 --- a/rust/ql/lib/codeql/rust/internal/PathResolution.qll +++ b/rust/ql/lib/codeql/rust/internal/PathResolution.qll @@ -568,6 +568,16 @@ class CrateItemNode extends NamedItemNode instanceof Crate { ) } + pragma[nomagic] + predicate isLatestVersion(string name) { + this = + max(CrateItemNode c, string ver | + name = c.getName() and ver = c.(Crate).getVersion() + | + c order by ver + ) + } + override string getName() { result = Crate.super.getName() } override Namespace getNamespace() { @@ -1529,11 +1539,11 @@ private predicate crateDependencyEdge(SourceFileItemNode file, string name, Crat crateDependency(file, name, dep) or // As a fallback, give all files access to crates that do not conflict with known dependencies - // and declarations. This is in order to workaround incomplete crate dependency information - // provided by the extractor, as well as `CrateItemNode.getASourceFile()` being unable to map - // a given file to its crate (for example, if the file is `mod` imported inside a macro that the - // extractor is unable to expand). - name = dep.getName() and + // and declarations, as long as those crates have a unique latest version. + // This is in order to workaround incomplete crate dependency information provided by the extractor, + // as well as `CrateItemNode.getASourceFile()` being unable to map a given file to its crate (for + // example, if the file is `mod` imported inside a macro that the extractor is unable to expand). + dep = unique(CrateItemNode dep0 | dep0.isLatestVersion(name)) and not hasDeclOrDep(file, name) } @@ -2385,6 +2395,11 @@ private module Debug { useImportEdge(use, name, item, kind) } + predicate debugCrateDependencyEdge(SourceFileItemNode file, string name, CrateItemNode dep) { + file = getRelevantLocatable() and + crateDependencyEdge(file, name, dep) + } + ItemNode debugGetASuccessor(ItemNode i, string name, SuccessorKind kind) { i = getRelevantLocatable() and result = i.getASuccessor(name, kind, _) From 9207c4be562334eef2760e36e2aeaf86e5984f8b Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Thu, 3 Sep 2026 13:04:03 +0200 Subject: [PATCH 2/3] Address review comment --- go/ql/lib/semmle/go/dependencies/SemVer.qll | 34 ++++--------------- .../semmle/javascript/dependencies/SemVer.qll | 34 ++++--------------- .../codeql/rust/internal/PathResolution.qll | 3 +- shared/util/codeql/util/SemVer.qll | 26 ++++++++++++++ 4 files changed, 42 insertions(+), 55 deletions(-) create mode 100644 shared/util/codeql/util/SemVer.qll diff --git a/go/ql/lib/semmle/go/dependencies/SemVer.qll b/go/ql/lib/semmle/go/dependencies/SemVer.qll index 290617781ec0..74444f94d50b 100644 --- a/go/ql/lib/semmle/go/dependencies/SemVer.qll +++ b/go/ql/lib/semmle/go/dependencies/SemVer.qll @@ -5,6 +5,7 @@ overlay[local?] module; import semmle.go.dependencies.Dependencies +private import codeql.util.SemVer /** * A SemVer-formatted version string in a dependency. @@ -17,35 +18,35 @@ class DependencySemVer extends string { DependencySemVer() { this = dep.getDepVersion() and - normalized = normalizeSemver(this) + normalized = normalizeSemVer(this) } /** * Holds if this version may be before `last`. */ bindingset[last] - predicate maybeBefore(string last) { normalized < normalizeSemver(last) } + predicate maybeBefore(string last) { normalized < normalizeSemVer(last) } /** * Holds if this version may be after `first`. */ bindingset[first] - predicate maybeAfter(string first) { normalizeSemver(first) < normalized } + predicate maybeAfter(string first) { normalizeSemVer(first) < normalized } /** * Holds if this version may be between `first` (inclusive) and `last` (exclusive). */ bindingset[first, last] predicate maybeBetween(string first, string last) { - normalizeSemver(first) <= normalized and - normalized < normalizeSemver(last) + normalizeSemVer(first) <= normalized and + normalized < normalizeSemVer(last) } /** * Holds if this version is equivalent to `other`. */ bindingset[other] - predicate is(string other) { normalized = normalizeSemver(other) } + predicate is(string other) { normalized = normalizeSemVer(other) } /** * Gets the dependency that uses this string. @@ -53,27 +54,6 @@ class DependencySemVer extends string { Dependency getDependency() { result = dep } } -bindingset[str] -private string leftPad(string str) { result = ("000" + str).suffix(str.length()) } - -/** - * Normalizes a SemVer string such that the lexicographical ordering - * of two normalized strings is consistent with the SemVer ordering. - * - * Pre-release information and build metadata is not yet supported. - */ -bindingset[orig] -private string normalizeSemver(string orig) { - exists(string pattern, string major, string minor, string patch | - pattern = "v?(\\d+)\\.(\\d+)\\.(\\d+)(\\D.*)?" and - major = orig.regexpCapture(pattern, 1) and - minor = orig.regexpCapture(pattern, 2) and - patch = orig.regexpCapture(pattern, 3) - | - result = leftPad(major) + "." + leftPad(minor) + "." + leftPad(patch) - ) -} - /** * A version string in a dependency that has a SemVer, but also contains a git commit SHA. * diff --git a/javascript/ql/lib/semmle/javascript/dependencies/SemVer.qll b/javascript/ql/lib/semmle/javascript/dependencies/SemVer.qll index 1d091e9219c9..1ebbfad608fd 100644 --- a/javascript/ql/lib/semmle/javascript/dependencies/SemVer.qll +++ b/javascript/ql/lib/semmle/javascript/dependencies/SemVer.qll @@ -3,6 +3,7 @@ */ import semmle.javascript.dependencies.Dependencies +private import codeql.util.SemVer /** * A SemVer-formatted version string in a dependency. @@ -15,59 +16,38 @@ class DependencySemVer extends string { DependencySemVer() { dep.info(_, this) and - normalized = normalizeSemver(this) + normalized = normalizeSemVer(this) } /** * Holds if this version may be before `last`. */ bindingset[last] - predicate maybeBefore(string last) { normalized < normalizeSemver(last) } + predicate maybeBefore(string last) { normalized < normalizeSemVer(last) } /** * Holds if this version may be after `first`. */ bindingset[first] - predicate maybeAfter(string first) { normalizeSemver(first) < normalized } + predicate maybeAfter(string first) { normalizeSemVer(first) < normalized } /** * Holds if this version may be between `first` (inclusive) and `last` (exclusive). */ bindingset[first, last] predicate maybeBetween(string first, string last) { - normalizeSemver(first) <= normalized and - normalized < normalizeSemver(last) + normalizeSemVer(first) <= normalized and + normalized < normalizeSemVer(last) } /** * Holds if this version is equivalent to `other`. */ bindingset[other] - predicate is(string other) { normalized = normalizeSemver(other) } + predicate is(string other) { normalized = normalizeSemVer(other) } /** * Gets the dependency that uses this string. */ Dependency getDependency() { result = dep } } - -bindingset[str] -private string leftPad(string str) { result = ("000" + str).suffix(str.length()) } - -/** - * Normalizes a SemVer string such that the lexicographical ordering - * of two normalized strings is consistent with the SemVer ordering. - * - * Pre-release information and build metadata is not yet supported. - */ -bindingset[orig] -private string normalizeSemver(string orig) { - exists(string pattern, string major, string minor, string patch | - pattern = "(\\d+)\\.(\\d+)\\.(\\d+)" and - major = orig.regexpCapture(pattern, 1) and - minor = orig.regexpCapture(pattern, 2) and - patch = orig.regexpCapture(pattern, 3) - | - result = leftPad(major) + "." + leftPad(minor) + "." + leftPad(patch) - ) -} diff --git a/rust/ql/lib/codeql/rust/internal/PathResolution.qll b/rust/ql/lib/codeql/rust/internal/PathResolution.qll index a79908ac6f00..fbc859843ceb 100644 --- a/rust/ql/lib/codeql/rust/internal/PathResolution.qll +++ b/rust/ql/lib/codeql/rust/internal/PathResolution.qll @@ -48,6 +48,7 @@ private import codeql.rust.elements.internal.CallExprImpl::Impl as CallExprImpl private import codeql.rust.internal.CachedStages private import codeql.rust.frameworks.stdlib.Builtins as Builtins private import codeql.util.Option +private import codeql.util.SemVer private newtype TNamespace = TTypeNamespace() or @@ -572,7 +573,7 @@ class CrateItemNode extends NamedItemNode instanceof Crate { predicate isLatestVersion(string name) { this = max(CrateItemNode c, string ver | - name = c.getName() and ver = c.(Crate).getVersion() + name = c.getName() and ver = normalizeSemVer(c.(Crate).getVersion()) | c order by ver ) diff --git a/shared/util/codeql/util/SemVer.qll b/shared/util/codeql/util/SemVer.qll new file mode 100644 index 000000000000..d412fa698fda --- /dev/null +++ b/shared/util/codeql/util/SemVer.qll @@ -0,0 +1,26 @@ +/** + * Provides logic for working SemVer (Semantic Versioning). + */ +overlay[local?] +module; + +bindingset[str] +private string leftPad(string str) { result = ("0000" + str).suffix(str.length()) } + +/** + * Normalizes a SemVer string such that the lexicographical ordering + * of two normalized strings is consistent with the SemVer ordering. + * + * Pre-release information and build metadata is not yet supported. + */ +bindingset[orig] +string normalizeSemVer(string orig) { + exists(string pattern, string major, string minor, string patch | + pattern = "v?(\\d+)\\.(\\d+)\\.(\\d+)(\\D.*)?" and + major = orig.regexpCapture(pattern, 1) and + minor = orig.regexpCapture(pattern, 2) and + patch = orig.regexpCapture(pattern, 3) + | + result = leftPad(major) + "." + leftPad(minor) + "." + leftPad(patch) + ) +} From df7d49d1e0aff059ee3e93bd72434ca7813a0c3b Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Thu, 3 Sep 2026 15:40:18 +0200 Subject: [PATCH 3/3] Rename `normalizeSemVer` to `padSemVer` and adopt Ruby implementation --- go/ql/lib/semmle/go/dependencies/SemVer.qll | 12 ++--- .../semmle/javascript/dependencies/SemVer.qll | 12 ++--- .../ql/lib/codeql/ruby/frameworks/Gemfile.qll | 50 +++---------------- .../codeql/rust/internal/PathResolution.qll | 2 +- shared/util/codeql/util/SemVer.qll | 49 ++++++++++++++---- 5 files changed, 59 insertions(+), 66 deletions(-) diff --git a/go/ql/lib/semmle/go/dependencies/SemVer.qll b/go/ql/lib/semmle/go/dependencies/SemVer.qll index 74444f94d50b..a55d4e127990 100644 --- a/go/ql/lib/semmle/go/dependencies/SemVer.qll +++ b/go/ql/lib/semmle/go/dependencies/SemVer.qll @@ -18,35 +18,35 @@ class DependencySemVer extends string { DependencySemVer() { this = dep.getDepVersion() and - normalized = normalizeSemVer(this) + normalized = padSemVer(this) } /** * Holds if this version may be before `last`. */ bindingset[last] - predicate maybeBefore(string last) { normalized < normalizeSemVer(last) } + predicate maybeBefore(string last) { normalized < padSemVer(last) } /** * Holds if this version may be after `first`. */ bindingset[first] - predicate maybeAfter(string first) { normalizeSemVer(first) < normalized } + predicate maybeAfter(string first) { padSemVer(first) < normalized } /** * Holds if this version may be between `first` (inclusive) and `last` (exclusive). */ bindingset[first, last] predicate maybeBetween(string first, string last) { - normalizeSemVer(first) <= normalized and - normalized < normalizeSemVer(last) + padSemVer(first) <= normalized and + normalized < padSemVer(last) } /** * Holds if this version is equivalent to `other`. */ bindingset[other] - predicate is(string other) { normalized = normalizeSemVer(other) } + predicate is(string other) { normalized = padSemVer(other) } /** * Gets the dependency that uses this string. diff --git a/javascript/ql/lib/semmle/javascript/dependencies/SemVer.qll b/javascript/ql/lib/semmle/javascript/dependencies/SemVer.qll index 1ebbfad608fd..a869856076d5 100644 --- a/javascript/ql/lib/semmle/javascript/dependencies/SemVer.qll +++ b/javascript/ql/lib/semmle/javascript/dependencies/SemVer.qll @@ -16,35 +16,35 @@ class DependencySemVer extends string { DependencySemVer() { dep.info(_, this) and - normalized = normalizeSemVer(this) + normalized = padSemVer(this) } /** * Holds if this version may be before `last`. */ bindingset[last] - predicate maybeBefore(string last) { normalized < normalizeSemVer(last) } + predicate maybeBefore(string last) { normalized < padSemVer(last) } /** * Holds if this version may be after `first`. */ bindingset[first] - predicate maybeAfter(string first) { normalizeSemVer(first) < normalized } + predicate maybeAfter(string first) { padSemVer(first) < normalized } /** * Holds if this version may be between `first` (inclusive) and `last` (exclusive). */ bindingset[first, last] predicate maybeBetween(string first, string last) { - normalizeSemVer(first) <= normalized and - normalized < normalizeSemVer(last) + padSemVer(first) <= normalized and + normalized < padSemVer(last) } /** * Holds if this version is equivalent to `other`. */ bindingset[other] - predicate is(string other) { normalized = normalizeSemVer(other) } + predicate is(string other) { normalized = padSemVer(other) } /** * Gets the dependency that uses this string. diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Gemfile.qll b/ruby/ql/lib/codeql/ruby/frameworks/Gemfile.qll index 83ebc27100a8..14153f8b16aa 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Gemfile.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Gemfile.qll @@ -3,6 +3,7 @@ */ private import codeql.ruby.AST +private import codeql.util.SemVer /** * Provides classes and predicates for Gemfiles, including version constraint logic. @@ -138,7 +139,7 @@ module Gemfile { exists(int thisMajor, int thisMinor, int otherMajor, int otherMinor | thisMajor = this.getVersion().getMajor() and thisMinor = this.getVersion().getMinor() and - exists(string maj, string mi | normalizeSemver(other, _, maj, mi, _) | + exists(string maj, string mi | exists(padSemVer(other, maj, mi, _)) | otherMajor = maj.toInt() and otherMinor = mi.toInt() ) | @@ -171,26 +172,26 @@ module Gemfile { Version() { this = any(Gem c).getAVersionConstraint().getVersionString() and - normalized = normalizeSemver(this) + normalized = padSemVer(this) } /** * Holds if this version is strictly before the version defined by `other`. */ bindingset[other] - predicate before(string other) { normalized < normalizeSemver(other) } + predicate before(string other) { normalized < padSemVer(other) } /** * Holds if this versino is equal to the version defined by `other`. */ bindingset[other] - predicate equal(string other) { normalized = normalizeSemver(other) } + predicate equal(string other) { normalized = padSemVer(other) } /** * Holds if this version is strictly after the version defined by `other`. */ bindingset[other] - predicate after(string other) { normalized > normalizeSemver(other) } + predicate after(string other) { normalized > padSemVer(other) } /** * Holds if this version defines a patch number. @@ -212,43 +213,4 @@ module Gemfile { */ int getPatch() { result = getPatch(normalized).toInt() } } - - /** - * Normalizes a SemVer string such that the lexicographical ordering - * of two normalized strings is consistent with the SemVer ordering. - * - * Pre-release information and build metadata is not supported. - */ - bindingset[orig] - private predicate normalizeSemver( - string orig, string normalized, string major, string minor, string patch - ) { - major = getMajor(orig) and - ( - minor = getMinor(orig) - or - not exists(getMinor(orig)) and minor = "0" - ) and - ( - patch = getPatch(orig) - or - not exists(getPatch(orig)) and patch = "0" - ) and - normalized = leftPad(major) + "." + leftPad(minor) + "." + leftPad(patch) - } - - bindingset[orig] - private string normalizeSemver(string orig) { normalizeSemver(orig, result, _, _, _) } - - bindingset[s] - private string getMajor(string s) { result = s.regexpCapture("(\\d+).*", 1) } - - bindingset[s] - private string getMinor(string s) { result = s.regexpCapture("(\\d+)\\.(\\d+).*", 2) } - - bindingset[s] - private string getPatch(string s) { result = s.regexpCapture("(\\d+)\\.(\\d+)\\.(\\d+).*", 3) } - - bindingset[str] - private string leftPad(string str) { result = ("000" + str).suffix(str.length()) } } diff --git a/rust/ql/lib/codeql/rust/internal/PathResolution.qll b/rust/ql/lib/codeql/rust/internal/PathResolution.qll index fbc859843ceb..f62262f7423f 100644 --- a/rust/ql/lib/codeql/rust/internal/PathResolution.qll +++ b/rust/ql/lib/codeql/rust/internal/PathResolution.qll @@ -573,7 +573,7 @@ class CrateItemNode extends NamedItemNode instanceof Crate { predicate isLatestVersion(string name) { this = max(CrateItemNode c, string ver | - name = c.getName() and ver = normalizeSemVer(c.(Crate).getVersion()) + name = c.getName() and ver = padSemVer(c.(Crate).getVersion()) | c order by ver ) diff --git a/shared/util/codeql/util/SemVer.qll b/shared/util/codeql/util/SemVer.qll index d412fa698fda..4c063721ce00 100644 --- a/shared/util/codeql/util/SemVer.qll +++ b/shared/util/codeql/util/SemVer.qll @@ -7,6 +7,24 @@ module; bindingset[str] private string leftPad(string str) { result = ("0000" + str).suffix(str.length()) } +/** + * Gets the major number of a SemVer string. + */ +bindingset[s] +string getMajor(string s) { result = s.regexpCapture("v?(\\d+).*", 1) } + +/** + * Gets the minor number of a SemVer string. + */ +bindingset[s] +string getMinor(string s) { result = s.regexpCapture("v?(\\d+)\\.(\\d+).*", 2) } + +/** + * Gets the patch number of a SemVer string. + */ +bindingset[s] +string getPatch(string s) { result = s.regexpCapture("v?(\\d+)\\.(\\d+)\\.(\\d+).*", 3) } + /** * Normalizes a SemVer string such that the lexicographical ordering * of two normalized strings is consistent with the SemVer ordering. @@ -14,13 +32,26 @@ private string leftPad(string str) { result = ("0000" + str).suffix(str.length() * Pre-release information and build metadata is not yet supported. */ bindingset[orig] -string normalizeSemVer(string orig) { - exists(string pattern, string major, string minor, string patch | - pattern = "v?(\\d+)\\.(\\d+)\\.(\\d+)(\\D.*)?" and - major = orig.regexpCapture(pattern, 1) and - minor = orig.regexpCapture(pattern, 2) and - patch = orig.regexpCapture(pattern, 3) - | - result = leftPad(major) + "." + leftPad(minor) + "." + leftPad(patch) - ) +string padSemVer(string orig, string major, string minor, string patch) { + major = getMajor(orig) and + ( + minor = getMinor(orig) + or + not exists(getMinor(orig)) and minor = "0" + ) and + ( + patch = getPatch(orig) + or + not exists(getPatch(orig)) and patch = "0" + ) and + result = leftPad(major) + "." + leftPad(minor) + "." + leftPad(patch) } + +/** + * Normalizes a SemVer string such that the lexicographical ordering + * of two normalized strings is consistent with the SemVer ordering. + * + * Pre-release information and build metadata is not yet supported. + */ +bindingset[orig] +string padSemVer(string orig) { result = padSemVer(orig, _, _, _) }