Skip to content

Commit 9770a12

Browse files
committed
fix(package): keep Swift regex literals out of the comment scanner
`#/foo//bar/#` is a valid extended regex literal with no comment in it, but the scanner only knew the `#"` raw-string family, so it read the literal's `//` as a line comment and shipped `let pattern = #/foo` — Swift that does not compile. Add `#/…/#` and `##/…/##` as a literal context: matching `#` counts, the single- and multi-line forms, Swift's own-line rule for a multi-line closing delimiter, and the `\/` escape that keeps one from closing early. Bare `/…/` literals stay unresolvable, because the same `/` opens a comment, divides, and starts a regex literal, and only the parse separates them. Where one could begin — an expression position whose `/` is not followed by a space, a tab or `)` — packaging throws by file and line instead of rewriting bytes it cannot prove are code. Divisions (`width/2`, `Double(3)/Double(4)`), the recording scripts' shebang and `(/)` keep flowing through.
1 parent d95e6ab commit 9770a12

3 files changed

Lines changed: 361 additions & 27 deletions

File tree

scripts/__tests__/strip-swift-comments.test.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,81 @@ test('preserves a multi-line raw literal and its line continuations', () => {
112112
assert.equal(strip(source), source);
113113
});
114114

115+
// Every Swift snippet in the regex-literal tests below parses clean under `xcrun swiftc -parse`
116+
// (Swift 6.2), and so does what the scanner leaves of it. `#/foo//bar/#` has no comment in it at
117+
// all: before the scanner knew the delimiter, it shipped `let pattern = #/foo`.
118+
test('preserves extended regex literals whose contents are comment-shaped', () => {
119+
const source = swift(
120+
'let pattern = #/foo//bar/#',
121+
'let pounded = ##/a//b/#c/##',
122+
'let blockish = #/x/*y/#',
123+
String.raw`let escaped = #/a\/#b/#`,
124+
);
125+
126+
assert.equal(strip(source), source);
127+
assert.equal(stripSwiftComments(source).removedComments, 0);
128+
});
129+
130+
test('strips a real comment that trails an extended regex literal', () => {
131+
const source = swift(
132+
'let trailing = #/a//b/# // trailing',
133+
'let blocked = ##/c/*d*/## /* block */',
134+
'let next = 1',
135+
);
136+
137+
assert.equal(
138+
strip(source),
139+
swift('let trailing = #/a//b/#', 'let blocked = ##/c/*d*/##', 'let next = 1'),
140+
);
141+
assert.equal(stripSwiftComments(source).removedComments, 2);
142+
});
143+
144+
test('preserves a multi-line extended regex literal verbatim, comment-shaped lines included', () => {
145+
const source = swift(
146+
'let multi = #/',
147+
' foo//bar',
148+
' /*e*/',
149+
String.raw` a\/#b`,
150+
'',
151+
' /#',
152+
'let after = 1 // note',
153+
);
154+
155+
assert.equal(
156+
strip(source),
157+
swift(
158+
'let multi = #/',
159+
' foo//bar',
160+
' /*e*/',
161+
String.raw` a\/#b`,
162+
'',
163+
' /#',
164+
'let after = 1',
165+
),
166+
);
167+
});
168+
169+
test('reads an unspaced division as an operator, not as a bare regex literal', () => {
170+
const source = swift(
171+
'#!/usr/bin/env swift',
172+
'let half = width/2 // note',
173+
'let ratio = Double(3)/Double(4)',
174+
'let spaced = width / 2 // also fine',
175+
'let divide: (Int, Int) -> Int = (/)',
176+
);
177+
178+
assert.equal(
179+
strip(source),
180+
swift(
181+
'#!/usr/bin/env swift',
182+
'let half = width/2',
183+
'let ratio = Double(3)/Double(4)',
184+
'let spaced = width / 2',
185+
'let divide: (Int, Int) -> Int = (/)',
186+
),
187+
);
188+
});
189+
115190
test('reads interpolation segments as code without losing their nested literals', () => {
116191
const source = swift(
117192
String.raw`let line = "prefix \(makeURL("https://example.com")) suffix" // trailing`,
@@ -231,3 +306,35 @@ test('throws when an interpolation segment never closes', () => {
231306
/Unterminated interpolation in Fixture\.swift/,
232307
);
233308
});
309+
310+
// A bare `/…/` is the one construct a scanner cannot resolve: Swift lexes a comment, a division
311+
// and a regex literal from the same `/`, and only the parse tells them apart. Packaging fails
312+
// rather than rewrite bytes it cannot prove are code.
313+
test('throws on a bare regex literal instead of reading its contents as a comment', () => {
314+
assert.throws(
315+
() => strip(swift('let a = 1', 'let pattern = /foo//bar/')),
316+
/Ambiguous bare regex literal or division in Fixture\.swift:2/,
317+
);
318+
assert.throws(
319+
() => strip(swift('func f() -> Regex<Substring> {', String.raw` return /x\/y/`, '}')),
320+
/Ambiguous bare regex literal or division in Fixture\.swift:2/,
321+
);
322+
});
323+
324+
test('throws on an unterminated extended regex literal', () => {
325+
assert.throws(
326+
() => strip(swift('let a = 1', 'let pattern = #/no closing', 'let b = 2 // note')),
327+
/Unterminated regex literal in Fixture\.swift:2/,
328+
);
329+
assert.throws(
330+
() => strip(swift('let a = 1', 'let pattern = #/', ' never closed')),
331+
/Unterminated regex literal in Fixture\.swift \(started at line 2\)/,
332+
);
333+
});
334+
335+
test('throws when a multi-line regex literal closes mid-line', () => {
336+
assert.throws(
337+
() => strip(swift('let multi = #/', ' mid/#line stays', ' /#')),
338+
/Multi-line regex literal in Fixture\.swift:1 closes mid-line at line 2/,
339+
);
340+
});

0 commit comments

Comments
 (0)