Repository: github/codeql
Pull request #19369: Rust: Extract `SelfParam`s from crate graph
Author: login: hvitved
Labels: name: Rust
description: Pull requests that update Rust code
Created: 2025-04-24T12:48:08Z | Merged: 2025-04-29T19:23:27Z

Written with the help from Copilot chat.

[DCA](https://github.com/github/codeql-dca-main/issues/28513) looks great; the `Missing call targets` metric goes down for all projects, without significant analysis slowdown.


Diff:
diff --git a/rust/extractor/src/crate_graph.rs b/rust/extractor/src/crate_graph.rs
index 0203e8adc47d..8122248aba3c 100644
--- a/rust/extractor/src/crate_graph.rs
+++ b/rust/extractor/src/crate_graph.rs
@@ -383,25 +383,54 @@ fn emit_function(
     assert_eq!(sig.binders.len(Interner), parameters.len());
     let sig = sig.skip_binders();
     let ty_vars = &[parameters];
+    let function_data = db.function_data(function);
+    let mut self_param = None;
     let params = sig
         .params()
         .iter()
-        .map(|p| {
+        .enumerate()
+        .filter_map(|(idx, p)| {
             let type_repr = emit_hir_ty(trap, db, ty_vars, p);
-            trap.emit(generated::Param {
-                id: trap::TrapId::Star,
-                attrs: vec![],
-                type_repr,
-                pat: None,
-            })
+
+            if idx == 0 && function_data.has_self_param() {
+                // Check if the self parameter is a reference
+                let (is_ref, is_mut) = match p.kind(Interner) {
+                    chalk_ir::TyKind::Ref(mutability, _, _) => {
+                        (true, matches!(mutability, chalk_ir::Mutability::Mut))
+                    }
+                    chalk_ir::TyKind::Raw(mutability, _) => {
+                        (false, matches!(mutability, chalk_ir::Mutability::Mut))
+                    }
+                    _ => (false, false),
+                };
+
+                self_param = Some(trap.emit(generated::SelfParam {
+                    id: trap::TrapId::Star,
+                    attrs: vec![],
+                    type_repr,
+                    is_ref,
+                    is_mut,
+                    lifetime: None,
+                    name: None,
+                }));
+                None
+            } else {
+                Some(trap.emit(generated::Param {
+                    id: trap::TrapId::Star,
+                    attrs: vec![],
+                    type_repr,
+                    pat: None,
+                }))
+            }
         })
         .collect();
 
     let ret_type = emit_hir_ty(trap, db, ty_vars, sig.ret());
+
     let param_list = trap.emit(generated::ParamList {
         id: trap::TrapId::Star,
         params,
-        self_param: None,
+        self_param,
     });
     let ret_type = ret_type.map(|ret_type| {
         trap.emit(generated::RetTypeRepr {
diff --git a/rust/ql/lib/codeql/rust/frameworks/stdlib/Clone.qll b/rust/ql/lib/codeql/rust/frameworks/stdlib/Clone.qll
index 0798343837e0..514c3781355e 100644
--- a/rust/ql/lib/codeql/rust/frameworks/stdlib/Clone.qll
+++ b/rust/ql/lib/codeql/rust/frameworks/stdlib/Clone.qll
@@ -18,7 +18,9 @@ final class CloneCallable extends SummarizedCallable::Range {
   final override predicate propagatesFlow(
     string input, string output, boolean preservesValue, string model
   ) {
-    input = "Argument[self]" and
+    // The `clone` method takes a `&self` parameter and dereferences it;
+    // make sure to not clone the reference itself
+    input = ["Argument[self].Reference", "Argument[self].WithoutReference"] and
     output = "ReturnValue" and
     preservesValue = true and
     model = "generated"
diff --git a/rust/ql/test/library-tests/dataflow/modeled/inline-flow.expected b/rust/ql/test/library-tests/dataflow/modeled/inline-flow.expected
index a7d350ee5acd..b5e19855cfa5 100644
--- a/rust/ql/test/library-tests/dataflow/modeled/inline-flow.expected
+++ b/rust/ql/test/library-tests/dataflow/modeled/inline-flow.expected
@@ -8,18 +8,22 @@ models
 | 7 | Summary: lang:core; crate::ptr::write; Argument[1]; Argument[0].Reference; value |
 edges
 | main.rs:12:9:12:9 | a [Some] | main.rs:13:10:13:19 | a.unwrap() | provenance | MaD:2 |
+| main.rs:12:9:12:9 | a [Some] | main.rs:14:13:14:13 | a [Some] | provenance |  |
 | main.rs:12:9:12:9 | a [Some] | main.rs:14:13:14:21 | a.clone() [Some] | provenance | MaD:1 |
 | main.rs:12:9:12:9 | a [Some] | main.rs:14:13:14:21 | a.clone() [Some] | provenance | generated |
 | main.rs:12:13:12:28 | Some(...) [Some] | main.rs:12:9:12:9 | a [Some] | provenance |  |
 | main.rs:12:18:12:27 | source(...) | main.rs:12:13:12:28 | Some(...) [Some] | provenance |  |
 | main.rs:14:9:14:9 | b [Some] | main.rs:15:10:15:19 | b.unwrap() | provenance | MaD:2 |
+| main.rs:14:13:14:13 | a [Some] | main.rs:14:13:14:21 | a.clone() [Some] | provenance | generated |
 | main.rs:14:13:14:21 | a.clone() [Some] | main.rs:14:9:14:9 | b [Some] | provenance |  |
 | main.rs:19:9:19:9 | a [Ok] | main.rs:20:10:20:19 | a.unwrap() | provenance | MaD:5 |
+| main.rs:19:9:19:9 | a [Ok] | main.rs:21:13:21:13 | a [Ok] | provenance |  |
 | main.rs:19:9:19:9 | a [Ok] | main.rs:21:13:21:21 | a.clone() [Ok] | provenance | MaD:4 |
 | main.rs:19:9:19:9 | a [Ok] | main.rs:21:13:21:21 | a.clone() [Ok] | provenance | generated |
 | main.rs:19:31:19:44 | Ok(...) [Ok] | main.rs:19:9:19:9 | a [Ok] | provenance |  |
 | main.rs:19:34:19:43 | source(...) | main.rs:19:31:19:44 | Ok(...) [Ok] | provenance |  |
 | main.rs:21:9:21:9 | b [Ok] | main.rs:22:10:22:19 | b.unwrap() | provenance | MaD:5 |
+| main.rs:21:13:21:13 | a [Ok] | main.rs:21:13:21:21 | a.clone() [Ok] | provenance | generated |
 | main.rs:21:13:21:21 | a.clone() [Ok] | main.rs:21:9:21:9 | b [Ok] | provenance |  |
 | main.rs:26:9:26:9 | a | main.rs:27:10:27:10 | a | provenance |  |
 | main.rs:26:9:26:9 | a | main.rs:28:13:28:21 | a.clone() | provenance | generated |
@@ -57,6 +61,7 @@ nodes
 | main.rs:12:18:12:27 | source(...) | semmle.label | source(...) |
 | main.rs:13:10:13:19 | a.unwrap() | semmle.label | a.unwrap() |
 | main.rs:14:9:14:9 | b [Some] | semmle.label | b [Some] |
+| main.rs:14:13:14:13 | a [Some] | semmle.label | a [Some] |
 | main.rs:14:13:14:21 | a.clone() [Some] | semmle.label | a.clone() [Some] |
 | main.rs:15:10:15:19 | b.unwrap() | semmle.label | b.unwrap() |
 | main.rs:19:9:19:9 | a [Ok] | semmle.label | a [Ok] |
@@ -64,6 +69,7 @@ nodes
 | main.rs:19:34:19:43 | source(...) | semmle.label | source(...) |
 | main.rs:20:10:20:19 | a.unwrap() | semmle.label | a.unwrap() |
 | main.rs:21:9:21:9 | b [Ok] | semmle.label | b [Ok] |
+| main.rs:21:13:21:13 | a [Ok] | semmle.label | a [Ok] |
 | main.rs:21:13:21:21 | a.clone() [Ok] | semmle.label | a.clone() [Ok] |
 | main.rs:22:10:22:19 | b.unwrap() | semmle.label | b.unwrap() |
 | main.rs:26:9:26:9 | a | semmle.label | a |
diff --git a/rust/ql/test/library-tests/type-inference/type-inference.ql b/rust/ql/test/library-tests/type-inference/type-inference.ql
index d83900e58402..2652699558ff 100644
--- a/rust/ql/test/library-tests/type-inference/type-inference.ql
+++ b/rust/ql/test/library-tests/type-inference/type-inference.ql
@@ -4,7 +4,8 @@ import codeql.rust.internal.TypeInference as TypeInference
 import TypeInference
 
 query predicate inferType(AstNode n, TypePath path, Type t) {
-  t = TypeInference::inferType(n, path)
+  t = TypeInference::inferType(n, path) and
+  n.fromSource()
 }
 
 module ResolveTest implements TestSig {
diff --git a/rust/ql/test/query-tests/security/CWE-089/CONSISTENCY/PathResolutionConsistency.expected b/rust/ql/test/query-tests/security/CWE-089/CONSISTENCY/PathResolutionConsistency.expected
new file mode 100644
index 000000000000..9567c4ea5177
--- /dev/null
+++ b/rust/ql/test/query-tests/security/CWE-089/CONSISTENCY/PathResolutionConsistency.expected
@@ -0,0 +1,13 @@
+multipleMethodCallTargets
+| sqlx.rs:65:30:65:52 | unsafe_query_2.as_str() | file://:0:0:0:0 | fn as_str |
+| sqlx.rs:65:30:65:52 | unsafe_query_2.as_str() | file://:0:0:0:0 | fn as_str |
+| sqlx.rs:65:30:65:52 | unsafe_query_2.as_str() | file://:0:0:0:0 | fn as_str |
+| sqlx.rs:65:30:65:52 | unsafe_query_2.as_str() | file://:0:0:0:0 | fn as_str |
+| sqlx.rs:65:30:65:52 | unsafe_query_2.as_str() | file://:0:0:0:0 | fn as_str |
+| sqlx.rs:65:30:65:52 | unsafe_query_2.as_str() | file://:0:0:0:0 | fn as_str |
+| sqlx.rs:76:29:76:51 | unsafe_query_2.as_str() | file://:0:0:0:0 | fn as_str |
+| sqlx.rs:76:29:76:51 | unsafe_query_2.as_str() | file://:0:0:0:0 | fn as_str |
+| sqlx.rs:76:29:76:51 | unsafe_query_2.as_str() | file://:0:0:0:0 | fn as_str |
+| sqlx.rs:76:29:76:51 | unsafe_query_2.as_str() | file://:0:0:0:0 | fn as_str |
+| sqlx.rs:76:29:76:51 | unsafe_query_2.as_str() | file://:0:0:0:0 | fn as_str |
+| sqlx.rs:76:29:76:51 | unsafe_query_2.as_str() | file://:0:0:0:0 | fn as_str |


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors the function emitting logic to extract the self parameter separately from the other parameters in Rust functions. Key changes include:
- Introducing a new variable to hold function_data via db.function_data(function).
- Refactoring the parameter iteration to use .enumerate() with filter_map to extract the self parameter when present.
- Updating the generated ParamList to include the extracted self parameter.

<details>
<summary>Files not reviewed (4)</summary>

* **rust/ql/lib/codeql/rust/frameworks/stdlib/Clone.qll**: Language not supported
* **rust/ql/test/library-tests/dataflow/modeled/inline-flow.expected**: Language not supported
* **rust/ql/test/library-tests/type-inference/type-inference.ql**: Language not supported
* **rust/ql/test/query-tests/security/CWE-089/CONSISTENCY/PathResolutionConsistency.expected**: Language not supported
</details>



state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T12:54:47Z
user: login: aibaars
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T16:00:44Z
user: login: aibaars
body: Looks good to me. I'm wondering whether we should populate the `name` and `lifetime` fields.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T16:02:52Z
user: login: hvitved
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T19:22:40Z

Review comments:
diff_hunk: @@ -383,25 +383,54 @@ fn emit_function(
     assert_eq!(sig.binders.len(Interner), parameters.len());
     let sig = sig.skip_binders();
     let ty_vars = &[parameters];
+    let function_data = db.function_data(function);
+    let mut self_param = None;
     let params = sig
         .params()
         .iter()
-        .map(|p| {
+        .enumerate()
+        .filter_map(|(idx, p)| {
             let type_repr = emit_hir_ty(trap, db, ty_vars, p);
-            trap.emit(generated::Param {
-                id: trap::TrapId::Star,
-                attrs: vec![],
-                type_repr,
-                pat: None,
-            })
+
+            if idx == 0 && function_data.has_self_param() {
path: rust/extractor/src/crate_graph.rs
user: login: Copilot
body: Consider extracting the self parameter logic into a separate helper function to improve clarity and reduce complexity in the parameter iteration block.
created_at: 2025-04-25T12:54:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -383,25 +383,54 @@ fn emit_function(
     assert_eq!(sig.binders.len(Interner), parameters.len());
     let sig = sig.skip_binders();
     let ty_vars = &[parameters];
+    let function_data = db.function_data(function);
+    let mut self_param = None;
     let params = sig
         .params()
         .iter()
-        .map(|p| {
+        .enumerate()
+        .filter_map(|(idx, p)| {
             let type_repr = emit_hir_ty(trap, db, ty_vars, p);
-            trap.emit(generated::Param {
-                id: trap::TrapId::Star,
-                attrs: vec![],
-                type_repr,
-                pat: None,
-            })
+
+            if idx == 0 && function_data.has_self_param() {
+                // Check if the self parameter is a reference
+                let (is_ref, is_mut) = match p.kind(Interner) {
+                    chalk_ir::TyKind::Ref(mutability, _, _) => {
+                        (true, matches!(mutability, chalk_ir::Mutability::Mut))
+                    }
+                    chalk_ir::TyKind::Raw(mutability, _) => {
+                        (false, matches!(mutability, chalk_ir::Mutability::Mut))
+                    }
+                    _ => (false, false),
+                };
+
+                self_param = Some(trap.emit(generated::SelfParam {
+                    id: trap::TrapId::Star,
+                    attrs: vec![],
+                    type_repr,
+                    is_ref,
+                    is_mut,
+                    lifetime: None,
+                    name: None,
path: rust/extractor/src/crate_graph.rs
user: login: aibaars
body: Shouldn't the `name` be simply `self`? Is it possible to populate the `lifetime` field, or is it not important?
created_at: 2025-04-29T16:00:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -383,25 +383,54 @@ fn emit_function(
     assert_eq!(sig.binders.len(Interner), parameters.len());
     let sig = sig.skip_binders();
     let ty_vars = &[parameters];
+    let function_data = db.function_data(function);
+    let mut self_param = None;
     let params = sig
         .params()
         .iter()
-        .map(|p| {
+        .enumerate()
+        .filter_map(|(idx, p)| {
             let type_repr = emit_hir_ty(trap, db, ty_vars, p);
-            trap.emit(generated::Param {
-                id: trap::TrapId::Star,
-                attrs: vec![],
-                type_repr,
-                pat: None,
-            })
+
+            if idx == 0 && function_data.has_self_param() {
+                // Check if the self parameter is a reference
+                let (is_ref, is_mut) = match p.kind(Interner) {
+                    chalk_ir::TyKind::Ref(mutability, _, _) => {
+                        (true, matches!(mutability, chalk_ir::Mutability::Mut))
+                    }
+                    chalk_ir::TyKind::Raw(mutability, _) => {
+                        (false, matches!(mutability, chalk_ir::Mutability::Mut))
+                    }
+                    _ => (false, false),
+                };
+
+                self_param = Some(trap.emit(generated::SelfParam {
+                    id: trap::TrapId::Star,
+                    attrs: vec![],
+                    type_repr,
+                    is_ref,
+                    is_mut,
+                    lifetime: None,
+                    name: None,
path: rust/extractor/src/crate_graph.rs
user: login: hvitved
body: I decided to not populate the name, since we also don't do that for non-`self` parameters. Lifetimes are currently not important.
created_at: 2025-04-29T19:22:40Z
author_association: CONTRIBUTOR

PR comments:
user: login: paldepind
created_at: 2025-04-29T15:41:38Z
body: I'm not sure if it's related or orthogonal or already fixed in this PR, but FWIW it seems to me that functions from dependencies also have one additional non-self parameter. When `f` is from a dependency doing `f.getParamList().getNumberOfParams()` seems to be 1 bigger than it should be, as if the `self` parameter is counted alongside normal parameters.

This just bit me as I added an arity check in method resolution and lost a bunch of results.
author_association: CONTRIBUTOR
user: login: hvitved
created_at: 2025-04-29T19:22:04Z
body: > I'm not sure if it's related or orthogonal or already fixed in this PR, but FWIW it seems to me that functions from dependencies also have one additional non-self parameter. When `f` is from a dependency doing `f.getParamList().getNumberOfParams()` seems to be 1 bigger than it should be, as if the `self` parameter is counted alongside normal parameters.

That should be resolved by this PR as well.
author_association: CONTRIBUTOR
user: login: paldepind
created_at: 2025-04-30T06:40:53Z
body: > That should be resolved by this PR as well.

Nice! Thanks.
author_association: CONTRIBUTOR