Skip to content

Commit 0c4e103

Browse files
authored
fix: prevent error with unclosed tag followed by LF or end of file (#2750)
#2742 The reason that it happens to LF but not CRLF is that CRLF has two characters, so it doesn't touch the range of the start tag name, and therefore won't trigger the magic string error. This should also improve auto-import performance a bit because TypeScript only needs to compute auto-imports for Button and not a global auto-import.
1 parent 0c341bd commit 0c4e103

File tree

12 files changed

+113
-11
lines changed

12 files changed

+113
-11
lines changed

packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
383383
if (editForThisFile?.edits.length) {
384384
const [first] = editForThisFile.edits;
385385
first.newText =
386-
getNewScriptStartTag(this.configManager.getConfig()) +
386+
getNewScriptStartTag(this.configManager.getConfig(), formatCodeBasis.newLine) +
387387
formatCodeBasis.baseIndent +
388388
first.newText.trimStart();
389389

packages/language-server/src/plugins/typescript/features/CompletionProvider.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,8 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionRe
595595
return tagNames.map((name) => ({
596596
label: name,
597597
kind: CompletionItemKind.Property,
598-
textEdit: TextEdit.replace(this.cloneRange(replacementRange), name)
598+
textEdit: TextEdit.replace(this.cloneRange(replacementRange), name),
599+
commitCharacters: []
599600
}));
600601
}
601602

@@ -1131,9 +1132,17 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionRe
11311132
}
11321133

11331134
const config = this.configManager.getConfig();
1135+
// Remove the empty line after the script tag because getNewScriptStartTag will always add one
1136+
let newText = change.newText;
1137+
if (newText[0] === '\r') {
1138+
newText = newText.substring(1);
1139+
}
1140+
if (newText[0] === '\n') {
1141+
newText = newText.substring(1);
1142+
}
11341143
return TextEdit.replace(
11351144
beginOfDocumentRange,
1136-
`${getNewScriptStartTag(config)}${change.newText}</script>${newLine}`
1145+
`${getNewScriptStartTag(config, newLine)}${newText}</script>${newLine}`
11371146
);
11381147
}
11391148

packages/language-server/src/plugins/typescript/features/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,10 +429,10 @@ export function findChildOfKind(node: ts.Node, kind: ts.SyntaxKind): ts.Node | u
429429
}
430430
}
431431

432-
export function getNewScriptStartTag(lsConfig: Readonly<LSConfig>) {
432+
export function getNewScriptStartTag(lsConfig: Readonly<LSConfig>, newLine: string) {
433433
const lang = lsConfig.svelte.defaultScriptLanguage;
434434
const scriptLang = lang === 'none' ? '' : ` lang="${lang}"`;
435-
return `<script${scriptLang}>${ts.sys.newLine}`;
435+
return `<script${scriptLang}>${newLine}`;
436436
}
437437

438438
export function checkRangeMappingWithGeneratedSemi(

packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ describe('CompletionProviderImpl', function () {
298298
assert.deepStrictEqual(item, <CompletionItem>{
299299
label: 'custom-element',
300300
kind: CompletionItemKind.Property,
301+
commitCharacters: [],
301302
textEdit: {
302303
range: { start: { line: 0, character: 1 }, end: { line: 0, character: 2 } },
303304
newText: 'custom-element'

packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export class Element {
4343
private isSelfclosing: boolean;
4444
public tagName: string;
4545
public child?: any;
46+
private tagNameEnd: number;
4647

4748
// Add const $$xxx = ... only if the variable name is actually used
4849
// in order to prevent "$$xxx is defined but never used" TS hints
@@ -74,7 +75,7 @@ export class Element {
7475
this.startTagStart = this.node.start;
7576
this.startTagEnd = this.computeStartTagEnd();
7677

77-
const tagEnd = this.startTagStart + this.node.name.length + 1;
78+
const tagEnd = (this.tagNameEnd = this.startTagStart + this.node.name.length + 1);
7879
// Ensure deleted characters are mapped to the attributes object so we
7980
// get autocompletion when triggering it on a whitespace.
8081
if (/\s/.test(str.original.charAt(tagEnd))) {
@@ -205,7 +206,19 @@ export class Element {
205206
}
206207

207208
if (this.isSelfclosing) {
208-
transform(this.str, this.startTagStart, this.startTagEnd, [
209+
// The transformation is the whole start tag + <, ex: <span
210+
// To avoid the end tag transform being moved to before the tag name,
211+
// manually remove the first character and let the `transform` function skip removing unused
212+
let transformEnd = this.startTagEnd;
213+
if (
214+
this.str.original[transformEnd - 1] !== '>' &&
215+
(transformEnd === this.tagNameEnd || transformEnd === this.tagNameEnd + 1)
216+
) {
217+
transformEnd = this.startTagStart;
218+
this.str.remove(this.startTagStart, this.startTagStart + 1);
219+
}
220+
221+
transform(this.str, this.startTagStart, transformEnd, [
209222
// Named slot transformations go first inside a outer block scope because
210223
// <div let:xx {x} /> means "use the x of let:x", and without a separate
211224
// block scope this would give a "used before defined" error
@@ -295,7 +308,7 @@ export class Element {
295308
default: {
296309
createElementStatement = [
297310
`${createElement}("`,
298-
[this.node.start + 1, this.node.start + 1 + this.node.name.length],
311+
[this.node.start + 1, this.tagNameEnd],
299312
`"${addActions()}, {`
300313
];
301314
break;

packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export class InlineComponent {
3838
private startTagStart: number;
3939
private startTagEnd: number;
4040
private isSelfclosing: boolean;
41+
private tagNameEnd: number;
4142
public child?: any;
4243

4344
// Add const $$xxx = ... only if the variable name is actually used
@@ -64,7 +65,7 @@ export class InlineComponent {
6465
this.startTagStart = this.node.start;
6566
this.startTagEnd = this.computeStartTagEnd();
6667

67-
const tagEnd = this.startTagStart + this.node.name.length + 1;
68+
const tagEnd = (this.tagNameEnd = this.startTagStart + this.node.name.length + 1);
6869
// Ensure deleted characters are mapped to the attributes object so we
6970
// get autocompletion when triggering it on a whitespace.
7071
if (/\s/.test(str.original.charAt(tagEnd))) {
@@ -227,7 +228,7 @@ export class InlineComponent {
227228
if (endStart === -1) {
228229
// Can happen in loose parsing mode when there's no closing tag
229230
endStart = this.node.end;
230-
this.startTagEnd = this.node.end - 1;
231+
this.startTagEnd = Math.max(this.node.end - 1, this.tagNameEnd);
231232
} else {
232233
endStart += this.node.start;
233234
}
@@ -238,7 +239,17 @@ export class InlineComponent {
238239
}
239240
this.endTransformation.push('}');
240241

241-
transform(this.str, this.startTagStart, this.startTagEnd, [
242+
let transformationEnd = this.startTagEnd;
243+
244+
// The transformation is the whole start tag + <, ex: <Comp
245+
// To avoid the "cannot move inside itself" error,
246+
// manually remove the first character and let the transform function skip removing unused
247+
if (transformationEnd === this.tagNameEnd) {
248+
transformationEnd = this.startTagStart;
249+
this.str.remove(this.startTagStart, this.startTagStart + 1);
250+
}
251+
252+
transform(this.str, this.startTagStart, transformationEnd, [
242253
// See comment above why this goes first
243254
...namedSlotLetTransformation,
244255
...this.startTransformation,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"code": "expected_token",
3+
"message": "Expected token >\nhttps://svelte.dev/e/expected_token",
4+
"filename": "(unknown)",
5+
"start": {
6+
"line": 7,
7+
"column": 2,
8+
"character": 138
9+
},
10+
"end": {
11+
"line": 7,
12+
"column": 2,
13+
"character": 138
14+
},
15+
"position": [
16+
138,
17+
138
18+
],
19+
"frame": " 5: <div>\n 6: <Comp\n 7: </div>\n ^\n 8: \n 9: <Comp"
20+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
{ svelteHTML.createElement("div", {});
3+
{ const $$_pmoC1C = __sveltets_2_ensureComponent(Comp); new $$_pmoC1C({ target: __sveltets_2_any(), props: {}});} }
4+
5+
{ const $$_pmoC0C = __sveltets_2_ensureComponent(Comp); new $$_pmoC0C({ target: __sveltets_2_any(), props: {}});}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!--
2+
Don't add trailing newline to this file.
3+
A component ends where the file ends is a part of the test.
4+
-->
5+
<div>
6+
<Comp
7+
</div>
8+
9+
<Comp
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"code": "expected_token",
3+
"message": "Expected token >\nhttps://svelte.dev/e/expected_token",
4+
"filename": "(unknown)",
5+
"start": {
6+
"line": 7,
7+
"column": 2,
8+
"character": 138
9+
},
10+
"end": {
11+
"line": 7,
12+
"column": 2,
13+
"character": 138
14+
},
15+
"position": [
16+
138,
17+
138
18+
],
19+
"frame": " 5: <div>\n 6: <span\n 7: </div>\n ^\n 8: \n 9: <span"
20+
}

0 commit comments

Comments
 (0)