Skip to content

Commit 52ef9da

Browse files
committed
Change parser to allow parsing two script tags on <script setup>
1 parent c96d66e commit 52ef9da

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+5347
-16
lines changed

Diff for: src/index.ts

+25-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as path from "path"
77
import * as AST from "./ast"
88
import { LocationCalculator } from "./common/location-calculator"
99
import { HTMLParser, HTMLTokenizer } from "./html"
10-
import { parseScript, parseScriptElement } from "./script"
10+
import { parseScript, parseScriptElement, parseScriptElements } from "./script"
1111
import * as services from "./parser-services"
1212
import type { ParserOptions } from "./common/parser-options"
1313

@@ -68,6 +68,15 @@ function getLang(
6868
return lang || defaultLang
6969
}
7070

71+
/**
72+
* Checks whether the given script element is `<script setup>`.
73+
*/
74+
function isScriptSetup(script: AST.VElement): boolean {
75+
return script.startTag.attributes.some(
76+
(attr) => !attr.directive && attr.key.name === "setup",
77+
)
78+
}
79+
7180
/**
7281
* Parse the given source code.
7382
* @param code The source code to parse.
@@ -101,11 +110,12 @@ export function parseForESLint(
101110
const skipParsingScript = options.parser === false
102111
const tokenizer = new HTMLTokenizer(code, options)
103112
const rootAST = new HTMLParser(tokenizer, options).parse()
113+
104114
locationCalculator = new LocationCalculator(
105115
tokenizer.gaps,
106116
tokenizer.lineTerminators,
107117
)
108-
const script = rootAST.children.find(isScriptElement)
118+
const scripts = rootAST.children.filter(isScriptElement)
109119
const template = rootAST.children.find(isTemplateElement)
110120
const templateLang = getLang(template, "html")
111121
const concreteInfo: AST.HasConcreteInfo = {
@@ -118,10 +128,21 @@ export function parseForESLint(
118128
? Object.assign(template, concreteInfo)
119129
: undefined
120130

121-
if (skipParsingScript || script == null) {
131+
if (skipParsingScript || !scripts.length) {
122132
result = parseScript("", options)
133+
} else if (
134+
scripts.length === 2 &&
135+
scripts.some(isScriptSetup) &&
136+
scripts.some((e) => !isScriptSetup(e))
137+
) {
138+
result = parseScriptElements(
139+
scripts,
140+
code,
141+
new LocationCalculator([], tokenizer.lineTerminators),
142+
options,
143+
)
123144
} else {
124-
result = parseScriptElement(script, locationCalculator, options)
145+
result = parseScriptElement(scripts[0], locationCalculator, options)
125146
}
126147

127148
result.ast.templateBody = templateBody

Diff for: src/script/index.ts

+128-12
Original file line numberDiff line numberDiff line change
@@ -549,11 +549,10 @@ export function parseScriptElement(
549549
parserOptions: ParserOptions,
550550
): ESLintExtendedProgram {
551551
const text = node.children[0]
552-
const offset =
552+
const { code, offset } =
553553
text != null && text.type === "VText"
554-
? text.range[0]
555-
: node.startTag.range[1]
556-
const code = text != null && text.type === "VText" ? text.value : ""
554+
? { code: text.value, offset: text.range[0] }
555+
: { code: "", offset: node.startTag.range[1] }
557556
const locationCalculator =
558557
globalLocationCalculator.getSubCalculatorAfter(offset)
559558
const result = parseScriptFragment(code, locationCalculator, parserOptions)
@@ -564,14 +563,12 @@ export function parseScriptElement(
564563
const startTag = node.startTag
565564
const endTag = node.endTag
566565

567-
if (startTag != null) {
568-
result.ast.tokens.unshift({
569-
type: "Punctuator",
570-
range: startTag.range,
571-
loc: startTag.loc,
572-
value: "<script>",
573-
})
574-
}
566+
result.ast.tokens.unshift({
567+
type: "Punctuator",
568+
range: startTag.range,
569+
loc: startTag.loc,
570+
value: "<script>",
571+
})
575572
if (endTag != null) {
576573
result.ast.tokens.push({
577574
type: "Punctuator",
@@ -585,6 +582,125 @@ export function parseScriptElement(
585582
return result
586583
}
587584

585+
/**
586+
* Parse the source code of the given `<script>` elements.
587+
* @param nodes The `<script>` elements to parse.
588+
* @param code The source code of SFC.
589+
* @param globalLocationCalculatorWithoutGapOffsets The location calculator for fixLocations.
590+
* @param parserOptions The parser options.
591+
* @returns The result of parsing.
592+
*/
593+
export function parseScriptElements(
594+
nodes: VElement[],
595+
code: string,
596+
globalLocationCalculatorWithoutGapOffsets: LocationCalculator,
597+
parserOptions: ParserOptions,
598+
): ESLintExtendedProgram {
599+
let scriptCode = ""
600+
let startOffset = 0
601+
const tagTokens: Token[] = []
602+
603+
let firstScriptTextNode: Token | null = null
604+
for (const node of nodes) {
605+
const scriptTextNode = node.children[0]
606+
if (scriptTextNode.type === "VText") {
607+
const scriptText = code.slice(...scriptTextNode.range)
608+
if (!scriptCode) {
609+
firstScriptTextNode = scriptTextNode
610+
scriptCode += scriptText
611+
} else {
612+
const spaces = code
613+
.slice(startOffset, scriptTextNode.range[0])
614+
// eslint-disable-next-line require-unicode-regexp -- ignore u flag
615+
.replace(/[^\n\r\u2028\u2029 ]/g, " ")
616+
scriptCode += spaces + scriptText
617+
}
618+
startOffset = scriptTextNode.range[1]
619+
620+
const startTag = node.startTag
621+
const endTag = node.endTag
622+
tagTokens.push({
623+
type: "Punctuator",
624+
range: startTag.range,
625+
loc: startTag.loc,
626+
value: "<script>",
627+
})
628+
if (endTag != null) {
629+
tagTokens.push({
630+
type: "Punctuator",
631+
range: endTag.range,
632+
loc: endTag.loc,
633+
value: "</script>",
634+
})
635+
}
636+
}
637+
}
638+
const offset =
639+
firstScriptTextNode != null
640+
? firstScriptTextNode.range[0]
641+
: nodes[0].startTag.range[1]
642+
643+
const locationCalculator =
644+
globalLocationCalculatorWithoutGapOffsets.getSubCalculatorAfter(offset)
645+
const result = parseScriptFragment(
646+
scriptCode,
647+
locationCalculator,
648+
parserOptions,
649+
)
650+
651+
if (result.ast.comments != null) {
652+
for (const comment of result.ast.comments) {
653+
for (const tagToken of tagTokens) {
654+
checkIntersect(tagToken, comment)
655+
}
656+
}
657+
}
658+
if (result.ast.tokens != null) {
659+
const newTokens: Token[] = []
660+
let tagToken = tagTokens.shift()
661+
for (const token of result.ast.tokens) {
662+
while (tagToken && tagToken.range[0] < token.range[0]) {
663+
newTokens.push(tagToken)
664+
tagToken = tagTokens.shift()
665+
}
666+
checkIntersect(tagToken, token)
667+
newTokens.push(token)
668+
}
669+
if (tagToken) {
670+
newTokens.push(tagToken, ...tagTokens)
671+
}
672+
result.ast.tokens = newTokens
673+
}
674+
675+
return result
676+
677+
/**
678+
* Check if the tokens intersect.
679+
*/
680+
function checkIntersect(tagToken: Token | undefined, token: Token) {
681+
if (tagToken) {
682+
if (
683+
token.range[0] < tagToken.range[0] &&
684+
tagToken.range[0] < token.range[1]
685+
) {
686+
throw new ParseError(
687+
token.type === "Template"
688+
? "Unterminated template literal"
689+
: token.type === "Block"
690+
? "Unterminated comment"
691+
: token.type === "String"
692+
? "Unterminated string constant"
693+
: `Unterminated '${token.type}'`,
694+
undefined,
695+
tagToken.range[0],
696+
tagToken.loc.start.line,
697+
tagToken.loc.start.column,
698+
)
699+
}
700+
}
701+
}
702+
}
703+
588704
/**
589705
* Parse the source code of inline scripts.
590706
* @param code The source code of inline scripts.

0 commit comments

Comments
 (0)