Skip to content

Commit b2edf5d

Browse files
committed
Change parser to allow parsing two script tags on <script setup>
1 parent aa55d1c commit b2edf5d

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

+5348
-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

+129-12
Original file line numberDiff line numberDiff line change
@@ -550,11 +550,10 @@ export function parseScriptElement(
550550
parserOptions: ParserOptions,
551551
): ESLintExtendedProgram {
552552
const text = node.children[0]
553-
const offset =
553+
const { code, offset } =
554554
text != null && text.type === "VText"
555-
? text.range[0]
556-
: node.startTag.range[1]
557-
const code = text != null && text.type === "VText" ? text.value : ""
555+
? { code: text.value, offset: text.range[0] }
556+
: { code: "", offset: node.startTag.range[1] }
558557
const locationCalculator = globalLocationCalculator.getSubCalculatorAfter(
559558
offset,
560559
)
@@ -566,14 +565,12 @@ export function parseScriptElement(
566565
const startTag = node.startTag
567566
const endTag = node.endTag
568567

569-
if (startTag != null) {
570-
result.ast.tokens.unshift({
571-
type: "Punctuator",
572-
range: startTag.range,
573-
loc: startTag.loc,
574-
value: "<script>",
575-
})
576-
}
568+
result.ast.tokens.unshift({
569+
type: "Punctuator",
570+
range: startTag.range,
571+
loc: startTag.loc,
572+
value: "<script>",
573+
})
577574
if (endTag != null) {
578575
result.ast.tokens.push({
579576
type: "Punctuator",
@@ -587,6 +584,126 @@ export function parseScriptElement(
587584
return result
588585
}
589586

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

0 commit comments

Comments
 (0)