Skip to content

Commit c69135c

Browse files
geoffw0Copilot
andcommitted
unified: Decode Swift string literals for regex parsing
Preserve multi-segment constants as literals while keeping interpolations separate. Decode escaped, raw, Unicode, and multiline strings so regex parsing receives runtime pattern text rather than source syntax. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 41d387f commit c69135c

5 files changed

Lines changed: 124 additions & 40 deletions

File tree

‎unified/extractor/src/languages/swift/swift.rs‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -168,17 +168,23 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
168168
tree!((builtin_expr #{node}))
169169
}),
170170
rule!((simpleStringLiteralExpr) @@node => (string_literal #{node})),
171-
// String literal with a single constant segment (for some reason not typed as simpleStringLiteralExpr)
172171
rule!(
173-
(stringLiteralExpr segments: (stringSegment) segments: _* @@rest) @@node
174-
where rest.is_empty()
172+
(stringLiteralExpr segments: _* @@segments) @@node
175173
=>
176-
(string_literal #{node})
177-
),
178-
rule!(
179-
(stringLiteralExpr segments: _* @segments)
180-
=>
181-
(string_interpolation_expr element: {segments})
174+
expr {
175+
// Escapes and line breaks can split a constant literal into several segments.
176+
if segments.iter().all(|segment| {
177+
ctx.ast()
178+
.get_node(*segment)
179+
.expect("matched string segment must exist")
180+
.kind_name() == "stringSegment"
181+
}) {
182+
tree!((string_literal #{node}))
183+
} else {
184+
let segments = ctx.translate(segments)?;
185+
tree!((string_interpolation_expr element: {segments}))
186+
}
187+
}
182188
),
183189
rule!((stringSegment content: @@content) => (string_literal #{content})),
184190
rule!(

‎unified/extractor/tests/corpus/swift/literals/string-escapes.output‎

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -124,24 +124,15 @@ top_level source="⟨body⟩"
124124
variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩"
125125
modifier: modifier "let" source="let"
126126
pattern: identifier "escaped" source="escaped"
127-
value:
128-
string_interpolation_expr source="\"⟨element⟩⟨element⟩⟨element⟩\""
129-
element:
130-
string_literal "a\\n" source="a\\n"
131-
string_literal "b\\r" source="b\\r"
132-
string_literal "c" source="c"
127+
value: string_literal "\"a\\nb\\rc\"" source="\"a\\nb\\rc\""
133128
variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩"
134129
modifier: modifier "let" source="let"
135130
pattern: identifier "raw" source="raw"
136131
value: string_literal "#\"\\n\"#" source="#\"\\n\"#"
137132
variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩"
138133
modifier: modifier "let" source="let"
139134
pattern: identifier "multiline" source="multiline"
140-
value:
141-
string_interpolation_expr source="\"\"\"\n ⟨element⟩ ⟨element⟩\n \"\"\""
142-
element:
143-
string_literal "aa|\n" source="aa|\n"
144-
string_literal "bb" source="bb"
135+
value: string_literal "\"\"\"\n aa|\n bb\n \"\"\"" source="\"\"\"\n aa|\n bb\n \"\"\""
145136
variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩"
146137
modifier: modifier "let" source="let"
147138
pattern: identifier "interpolated" source="interpolated"

‎unified/ql/lib/codeql/unified/internal/FacadeAst.qll‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ private import unified as U
88
private import AstPlugin
99
private import codeql.files.FileSystem
1010
private import codeql.unified.internal.NameBinding as NameBinding
11+
private import codeql.unified.internal.SwiftStringLiterals as SwiftStringLiterals
1112

1213
module Unified {
1314
private import Ast::Unified as G
@@ -109,15 +110,11 @@ module Unified {
109110
class Expr extends G::Expr {
110111
/** Gets the string value of this expression, if it is a known string constant. */
111112
string getStringValue() {
112-
// TODO: we'll want to cook the string literals extractor-side, but for now
113-
// just strip the quotes here and ignore escape sequences.
114-
exists(string text | text = this.(StringLiteral).getValue() |
115-
result = text.regexpCapture("\"(.*)\"", 1)
116-
or
117-
// Constant-segments of string interpolations are represented as string literals, but their raw text does not have quotes
118-
not exists(text.regexpCapture("\"(.*)\"", 1)) and
119-
result = text
120-
)
113+
result = SwiftStringLiterals::getStringValue(this.(StringLiteral))
114+
or
115+
// Interpolation segments have no delimiters; their raw text is not a complete literal.
116+
this.getParent() instanceof StringInterpolationExpr and
117+
result = this.(StringLiteral).getValue()
121118
}
122119

123120
/** Gets the immediately-enclosing expression, skipping over intermediate sub-nodes like `Argument`, and without crossing a function boundary. */
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/** Provides decoded values of Swift string literals. */
2+
overlay[local?]
3+
module;
4+
5+
private import unified
6+
private import codeql.util.Numbers
7+
8+
private predicate literalContents(StringLiteral literal, string text, string escapePrefix) {
9+
exists(string raw, string hashes, string quote, string contents |
10+
raw = literal.getValue().replaceAll("\r\n", "\n").replaceAll("\r", "\n") and
11+
hashes = raw.regexpCapture("(?s)(#*)(\"\"\"|\")(.*)\\2\\1", 1) and
12+
quote = raw.regexpCapture("(?s)(#*)(\"\"\"|\")(.*)\\2\\1", 2) and
13+
contents = raw.regexpCapture("(?s)(#*)(\"\"\"|\")(.*)\\2\\1", 3) and
14+
escapePrefix = "\\" + hashes
15+
|
16+
quote = "\"" and text = contents
17+
or
18+
quote = "\"\"\"" and
19+
exists(string body, string indent |
20+
body = contents.regexpCapture("(?s)\n(.*)\n([ \t]*)", 1) and
21+
indent = contents.regexpCapture("(?s)\n(.*)\n([ \t]*)", 2) and
22+
text = body.regexpReplaceAll("(?m)^" + indent, "")
23+
)
24+
)
25+
}
26+
27+
private predicate decodedPart(StringLiteral literal, int start, int end, string value) {
28+
exists(string text, string prefix |
29+
literalContents(literal, text, prefix) and
30+
start in [0 .. text.length() - 1]
31+
|
32+
not text.substring(start, start + prefix.length()) = prefix and
33+
end = start + 1 and
34+
value = text.charAt(start)
35+
or
36+
text.substring(start, start + prefix.length()) = prefix and
37+
exists(int next | next = start + prefix.length() |
38+
end = next + 1 and
39+
(
40+
text.charAt(next) = "0" and value = 0.toUnicode()
41+
or
42+
text.charAt(next) = "t" and value = "\t"
43+
or
44+
text.charAt(next) = "n" and value = "\n"
45+
or
46+
text.charAt(next) = "r" and value = "\r"
47+
or
48+
value = text.charAt(next) and value = ["\\", "\"", "'"]
49+
)
50+
or
51+
exists(string hex |
52+
hex =
53+
text.substring(next, text.length()).regexpCapture("(?s)u\\{([0-9a-fA-F]{1,8})\\}.*", 1) and
54+
end = next + hex.length() + 3 and
55+
value = parseHexInt(hex).toUnicode()
56+
)
57+
or
58+
exists(string continuation |
59+
continuation = text.substring(next, text.length()).regexpCapture("(?s)([ \t]*\n).*", 1) and
60+
end = next + continuation.length() and
61+
value = ""
62+
)
63+
)
64+
)
65+
}
66+
67+
private predicate decodedOffset(StringLiteral literal, int offset) {
68+
offset = 0
69+
or
70+
decodedPart(literal, _, offset, _) and
71+
exists(int previous |
72+
decodedOffset(literal, previous) and
73+
decodedPart(literal, previous, offset, _)
74+
)
75+
}
76+
77+
/** Gets the decoded value of `literal`, if it is a complete, valid string literal. */
78+
string getStringValue(StringLiteral literal) {
79+
exists(string text |
80+
literalContents(literal, text, _) and
81+
decodedOffset(literal, text.length())
82+
|
83+
result =
84+
concat(int start, int end, string value |
85+
decodedOffset(literal, start) and decodedPart(literal, start, end, value)
86+
|
87+
value order by start
88+
)
89+
)
90+
}
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
| test.swift:1:5:1:9 | empty | |
22
| test.swift:2:5:2:10 | simple | abc |
3-
| test.swift:3:5:3:11 | escaped | |
4-
| test.swift:4:5:4:20 | escapedBackslash | \\\\n |
5-
| test.swift:5:5:5:11 | unicode | \\u{61}\\u{1F600} |
6-
| test.swift:6:5:6:7 | raw | #"\\n\\"abc"# |
7-
| test.swift:7:5:7:14 | rawEscapes | |
8-
| test.swift:8:5:8:16 | hashesInside | #"abc#def"# |
9-
| test.swift:9:5:9:13 | multiline | |
10-
| test.swift:13:5:13:19 | multilineIndent | |
11-
| test.swift:17:5:17:22 | multilineEmptyLine | |
12-
| test.swift:22:5:22:25 | multilineContinuation | |
13-
| test.swift:26:5:26:16 | rawMultiline | |
3+
| test.swift:3:5:3:11 | escaped | a\nb\rc\td\u0000e\\f"g'h |
4+
| test.swift:4:5:4:20 | escapedBackslash | \\n |
5+
| test.swift:5:5:5:11 | unicode | a\ud83d\ude00 |
6+
| test.swift:6:5:6:7 | raw | \\n\\"abc |
7+
| test.swift:7:5:7:14 | rawEscapes | \\n\na"\\ |
8+
| test.swift:8:5:8:16 | hashesInside | abc#def |
9+
| test.swift:9:5:9:13 | multiline | aa\|\nbb |
10+
| test.swift:13:5:13:19 | multilineIndent | aa\nbb |
11+
| test.swift:17:5:17:22 | multilineEmptyLine | aa\n\nbb |
12+
| test.swift:22:5:22:25 | multilineContinuation | aabb |
13+
| test.swift:26:5:26:16 | rawMultiline | aa\\n\nbb |
1414
| test.swift:30:5:30:17 | interpolation | |

0 commit comments

Comments
 (0)