Skip to content

Rust: Model String -> str implicit conversion in type inference #19737

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions rust/ql/lib/codeql/rust/frameworks/stdlib/Stdlib.qll
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,12 @@ class FutureTrait extends Trait {
result.getName().getText() = "Output"
}
}

/**
* The [`String` struct][1].
*
* [1]: https://doc.rust-lang.org/std/string/struct.String.html
*/
class StringStruct extends Struct {
StringStruct() { this.getCanonicalPath() = "alloc::string::String" }
}
43 changes: 32 additions & 11 deletions rust/ql/lib/codeql/rust/internal/TypeInference.qll
Original file line number Diff line number Diff line change
Expand Up @@ -780,12 +780,18 @@ private Type inferCallExprBaseType(AstNode n, TypePath path) {
not (path0.isEmpty() and result = TRefType()) and
path = TypePath::cons(TRefTypeParameter(), path0)
else (
not path0.isCons(TRefTypeParameter(), _) and
not (path0.isEmpty() and result = TRefType()) and
path = path0
or
// adjust for implicit borrow
path0.isCons(TRefTypeParameter(), path)
not (
receiverType.(StructType).asItemNode() instanceof StringStruct and
result.(StructType).asItemNode() instanceof Builtins::Str
) and
(
not path0.isCons(TRefTypeParameter(), _) and
not (path0.isEmpty() and result = TRefType()) and
path = path0
or
// adjust for implicit borrow
path0.isCons(TRefTypeParameter(), path)
)
)
)
else path = path0
Expand Down Expand Up @@ -1130,12 +1136,27 @@ final class MethodCall extends Call {
Type getTypeAt(TypePath path) {
if this.receiverImplicitlyBorrowed()
then
exists(TypePath path0 | result = inferType(super.getReceiver(), path0) |
path0.isCons(TRefTypeParameter(), path)
exists(TypePath path0, Type t0 |
t0 = inferType(super.getReceiver(), path0) and
(
path0.isCons(TRefTypeParameter(), path)
or
not path0.isCons(TRefTypeParameter(), _) and
not (path0.isEmpty() and result = TRefType()) and
path = path0
)
|
result = t0
or
not path0.isCons(TRefTypeParameter(), _) and
not (path0.isEmpty() and result = TRefType()) and
path = path0
// We do not yet model the `Deref` trait, so we hard-code the fact that
// `String` dereferences to `str` here. This allows us e.g. to resolve
// `x.parse::<usize>()` to the function `<core::str>::parse` when `x` has
// type `String`.
//
// See also https://doc.rust-lang.org/reference/expressions/method-call-expr.html#r-expr.method.autoref-deref
path.isEmpty() and
t0.(StructType).asItemNode() instanceof StringStruct and
result.(StructType).asItemNode() instanceof Builtins::Str
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it OK that we also still get the result from result = t0 as well in this situation, i.e. that we get two results (of which only one applies depending on context)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that is deliberate.

)
else result = inferType(super.getReceiver(), path)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,8 @@ readStep
| main.rs:442:25:442:29 | names | file://:0:0:0:0 | element | main.rs:442:9:442:20 | TuplePat |
| main.rs:444:41:444:67 | [post] \|...\| ... | main.rs:441:9:441:20 | captured default_name | main.rs:444:41:444:67 | [post] default_name |
| main.rs:444:44:444:55 | this | main.rs:441:9:441:20 | captured default_name | main.rs:444:44:444:55 | default_name |
| main.rs:469:13:469:13 | [post] receiver for b | file://:0:0:0:0 | &ref | main.rs:469:13:469:13 | [post] b |
| main.rs:470:19:470:19 | [post] receiver for b | file://:0:0:0:0 | &ref | main.rs:470:19:470:19 | [post] b |
| main.rs:481:10:481:11 | vs | file://:0:0:0:0 | element | main.rs:481:10:481:14 | vs[0] |
| main.rs:482:11:482:35 | ... .unwrap() | file://:0:0:0:0 | &ref | main.rs:482:10:482:35 | * ... |
| main.rs:483:11:483:35 | ... .unwrap() | file://:0:0:0:0 | &ref | main.rs:483:10:483:35 | * ... |
Expand Down Expand Up @@ -1058,6 +1060,8 @@ storeStep
| main.rs:429:30:429:30 | 3 | file://:0:0:0:0 | element | main.rs:429:23:429:31 | [...] |
| main.rs:432:18:432:27 | source(...) | file://:0:0:0:0 | element | main.rs:432:5:432:11 | [post] mut_arr |
| main.rs:444:41:444:67 | default_name | main.rs:441:9:441:20 | captured default_name | main.rs:444:41:444:67 | \|...\| ... |
| main.rs:469:13:469:13 | b | file://:0:0:0:0 | &ref | main.rs:469:13:469:13 | receiver for b |
| main.rs:470:19:470:19 | b | file://:0:0:0:0 | &ref | main.rs:470:19:470:19 | receiver for b |
| main.rs:479:15:479:24 | source(...) | file://:0:0:0:0 | element | main.rs:479:14:479:34 | [...] |
| main.rs:479:27:479:27 | 2 | file://:0:0:0:0 | element | main.rs:479:14:479:34 | [...] |
| main.rs:479:30:479:30 | 3 | file://:0:0:0:0 | element | main.rs:479:14:479:34 | [...] |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ multipleMethodCallTargets
| test_futures_io.rs:92:26:92:63 | pinned.poll_read(...) | file://:0:0:0:0 | fn poll_read |
| test_futures_io.rs:115:22:115:50 | pinned.poll_fill_buf(...) | file://:0:0:0:0 | fn poll_fill_buf |
| test_futures_io.rs:115:22:115:50 | pinned.poll_fill_buf(...) | file://:0:0:0:0 | fn poll_fill_buf |
| web_frameworks.rs:88:14:88:23 | a.as_str() | file://:0:0:0:0 | fn as_str |
| web_frameworks.rs:88:14:88:23 | a.as_str() | file://:0:0:0:0 | fn as_str |
| web_frameworks.rs:89:14:89:25 | a.as_bytes() | file://:0:0:0:0 | fn as_bytes |
| web_frameworks.rs:89:14:89:25 | a.as_bytes() | file://:0:0:0:0 | fn as_bytes |
multiplePathResolutions
| test.rs:112:62:112:73 | ...::from | file://:0:0:0:0 | fn from |
| test.rs:112:62:112:73 | ...::from | file://:0:0:0:0 | fn from |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
multipleMethodCallTargets
| main.rs:64:16:64:25 | s.as_str() | file://:0:0:0:0 | fn as_str |
| main.rs:64:16:64:25 | s.as_str() | file://:0:0:0:0 | fn as_str |
multiplePathResolutions
| main.rs:52:11:52:22 | ...::from | file://:0:0:0:0 | fn from |
| main.rs:52:11:52:22 | ...::from | file://:0:0:0:0 | fn from |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
multipleMethodCallTargets
| test.rs:55:7:55:26 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:55:7:55:26 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:56:7:56:21 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:56:7:56:21 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:72:7:72:26 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:72:7:72:26 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:73:7:73:36 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:73:7:73:36 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:74:7:74:34 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:74:7:74:34 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:75:7:75:27 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:75:7:75:27 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:254:7:254:36 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:254:7:254:36 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:256:7:256:33 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:256:7:256:33 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:257:7:257:36 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:257:7:257:36 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:258:7:258:26 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:258:7:258:26 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:262:7:262:28 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:262:7:262:28 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:263:7:263:37 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:263:7:263:37 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:264:7:264:36 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:264:7:264:36 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:267:7:267:32 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:267:7:267:32 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:277:7:277:34 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:277:7:277:34 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:280:7:280:36 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:280:7:280:36 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:284:7:284:39 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:284:7:284:39 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:291:7:291:53 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:291:7:291:53 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:292:7:292:45 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:292:7:292:45 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:294:7:294:39 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:294:7:294:39 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:295:7:295:34 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:295:7:295:34 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:296:7:296:42 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:296:7:296:42 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:298:7:298:48 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:298:7:298:48 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:299:7:299:35 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:299:7:299:35 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:300:7:300:35 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:300:7:300:35 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:339:7:339:39 | ... .as_str() | file://:0:0:0:0 | fn as_str |
| test.rs:339:7:339:39 | ... .as_str() | file://:0:0:0:0 | fn as_str |
5 changes: 5 additions & 0 deletions rust/ql/test/library-tests/type-inference/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,11 @@ mod method_call_type_conversion {
// implicit dereference handling doesn't affect nested borrows.
let t = x7.m1(); // $ method=m1 type=t:& type=t:&T.S2
println!("{:?}", x7);

let x9 : String = "Hello".to_string(); // $ type=x9:String
// Implicit `String` -> `str` conversion happens via the `Deref` trait:
// https://doc.rust-lang.org/std/string/struct.String.html#deref.
let u = x9.parse::<u32>(); // $ method=parse type=u:T.u32
}
}

Expand Down
Loading