Skip to content

Commit 62b6986

Browse files
authored
Add support for scope analysis of <script setup> (#144)
* Add support for scope analysis of `<script setup>` * update * fix for win test * Support for Compiler Macro * rename * refactor * refactor
1 parent d900ec2 commit 62b6986

Some content is hidden

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

46 files changed

+995
-122
lines changed

Diff for: .eslintignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/.nyc_output
22
/.temp
33
/coverage
4-
/node_modules
4+
node_modules
55
/src/external
66
/src/html/util
77
/test/fixtures

Diff for: .gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/.nyc_output
22
/.temp
33
/coverage
4-
/node_modules
4+
node_modules
55
/test/temp
66
/index.*
77
/npm-debug.log
88
/test.js
99
/test/fixtures/espree-v8/node_modules
10+
/test/fixtures/integrations/**/_actual.json

Diff for: scripts/update-fixtures-ast.js

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const PARSER_OPTIONS = {
2727
loc: true,
2828
range: true,
2929
tokens: true,
30+
eslintScopeManager: true,
3031
}
3132

3233
/**

Diff for: src/common/parser-options.ts

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export interface ParserOptions {
3535
range?: boolean
3636
tokens?: boolean
3737

38+
// From ESLint
39+
eslintScopeManager?: boolean
40+
3841
// others
3942
// [key: string]: any
4043
}

Diff for: src/index.ts

+120-92
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
isTemplateElement,
2424
} from "./common/ast-utils"
2525
import { parseStyleElements } from "./style"
26+
import { analyzeScope } from "./script/scope-analyzer"
27+
import { analyzeScriptSetupScope } from "./script-setup/scope-analyzer"
2628

2729
const STARTS_WITH_LT = /^\s*</u
2830

@@ -61,101 +63,11 @@ export function parseForESLint(
6163
let document: AST.VDocumentFragment | null
6264
let locationCalculator: LocationCalculatorForHtml | null
6365
if (!isVueFile(code, options)) {
64-
result = parseScript(code, {
65-
...options,
66-
ecmaVersion: options.ecmaVersion || DEFAULT_ECMA_VERSION,
67-
parser: getScriptParser(options.parser, () => {
68-
const ext = (
69-
path
70-
.extname(options.filePath || "unknown.js")
71-
.toLowerCase() || ""
72-
)
73-
// remove dot
74-
.slice(1)
75-
if (/^[jt]sx$/u.test(ext)) {
76-
return [ext, ext.slice(0, -1)]
77-
}
78-
79-
return ext
80-
}),
81-
})
66+
result = parseAsScript(code, options)
8267
document = null
8368
locationCalculator = null
8469
} else {
85-
const optionsForTemplate = {
86-
...options,
87-
ecmaVersion: options.ecmaVersion || DEFAULT_ECMA_VERSION,
88-
}
89-
const skipParsingScript = options.parser === false
90-
const tokenizer = new HTMLTokenizer(code, optionsForTemplate)
91-
const rootAST = new HTMLParser(tokenizer, optionsForTemplate).parse()
92-
93-
locationCalculator = new LocationCalculatorForHtml(
94-
tokenizer.gaps,
95-
tokenizer.lineTerminators,
96-
)
97-
const scripts = rootAST.children.filter(isScriptElement)
98-
const template = rootAST.children.find(isTemplateElement)
99-
const templateLang = getLang(template) || "html"
100-
const concreteInfo: AST.HasConcreteInfo = {
101-
tokens: rootAST.tokens,
102-
comments: rootAST.comments,
103-
errors: rootAST.errors,
104-
}
105-
const templateBody =
106-
template != null && templateLang === "html"
107-
? Object.assign(template, concreteInfo)
108-
: undefined
109-
110-
const scriptParser = getScriptParser(options.parser, () =>
111-
getParserLangFromSFC(rootAST),
112-
)
113-
let scriptSetup: VElement | undefined
114-
if (skipParsingScript || !scripts.length) {
115-
result = parseScript("", {
116-
...options,
117-
ecmaVersion: options.ecmaVersion || DEFAULT_ECMA_VERSION,
118-
parser: scriptParser,
119-
})
120-
} else if (
121-
scripts.length === 2 &&
122-
(scriptSetup = scripts.find(isScriptSetupElement))
123-
) {
124-
result = parseScriptSetupElements(
125-
scriptSetup,
126-
scripts.find((e) => e !== scriptSetup)!,
127-
code,
128-
new LinesAndColumns(tokenizer.lineTerminators),
129-
{
130-
...options,
131-
parser: scriptParser,
132-
},
133-
)
134-
} else {
135-
result = parseScriptElement(
136-
scripts[0],
137-
code,
138-
new LinesAndColumns(tokenizer.lineTerminators),
139-
{
140-
...options,
141-
parser: scriptParser,
142-
},
143-
)
144-
}
145-
146-
if (options.vueFeatures?.styleCSSVariableInjection ?? true) {
147-
const styles = rootAST.children.filter(isStyleElement)
148-
parseStyleElements(styles, locationCalculator, {
149-
...options,
150-
parser: getScriptParser(options.parser, function* () {
151-
yield "<template>"
152-
yield getParserLangFromSFC(rootAST)
153-
}),
154-
})
155-
}
156-
157-
result.ast.templateBody = templateBody
158-
document = rootAST
70+
;({ result, document, locationCalculator } = parseAsSFC(code, options))
15971
}
16072

16173
result.services = Object.assign(
@@ -179,3 +91,119 @@ export function parse(code: string, options: any): AST.ESLintProgram {
17991
}
18092

18193
export { AST }
94+
95+
function parseAsSFC(code: string, options: ParserOptions) {
96+
const optionsForTemplate = {
97+
...options,
98+
ecmaVersion: options.ecmaVersion || DEFAULT_ECMA_VERSION,
99+
}
100+
const skipParsingScript = options.parser === false
101+
const tokenizer = new HTMLTokenizer(code, optionsForTemplate)
102+
const rootAST = new HTMLParser(tokenizer, optionsForTemplate).parse()
103+
104+
const locationCalculator = new LocationCalculatorForHtml(
105+
tokenizer.gaps,
106+
tokenizer.lineTerminators,
107+
)
108+
const scripts = rootAST.children.filter(isScriptElement)
109+
const template = rootAST.children.find(isTemplateElement)
110+
const templateLang = getLang(template) || "html"
111+
const concreteInfo: AST.HasConcreteInfo = {
112+
tokens: rootAST.tokens,
113+
comments: rootAST.comments,
114+
errors: rootAST.errors,
115+
}
116+
const templateBody =
117+
template != null && templateLang === "html"
118+
? Object.assign(template, concreteInfo)
119+
: undefined
120+
121+
const scriptParser = getScriptParser(options.parser, () =>
122+
getParserLangFromSFC(rootAST),
123+
)
124+
let result: AST.ESLintExtendedProgram
125+
let scriptSetup: VElement | undefined
126+
if (skipParsingScript || !scripts.length) {
127+
result = parseScript("", {
128+
...options,
129+
ecmaVersion: options.ecmaVersion || DEFAULT_ECMA_VERSION,
130+
parser: scriptParser,
131+
})
132+
} else if (
133+
scripts.length === 2 &&
134+
(scriptSetup = scripts.find(isScriptSetupElement))
135+
) {
136+
result = parseScriptSetupElements(
137+
scriptSetup,
138+
scripts.find((e) => e !== scriptSetup)!,
139+
code,
140+
new LinesAndColumns(tokenizer.lineTerminators),
141+
{
142+
...options,
143+
parser: scriptParser,
144+
},
145+
)
146+
} else {
147+
result = parseScriptElement(
148+
scripts[0],
149+
code,
150+
new LinesAndColumns(tokenizer.lineTerminators),
151+
{
152+
...options,
153+
parser: scriptParser,
154+
},
155+
)
156+
}
157+
158+
if (options.vueFeatures?.styleCSSVariableInjection ?? true) {
159+
const styles = rootAST.children.filter(isStyleElement)
160+
parseStyleElements(styles, locationCalculator, {
161+
...options,
162+
parser: getScriptParser(options.parser, function* () {
163+
yield "<template>"
164+
yield getParserLangFromSFC(rootAST)
165+
}),
166+
})
167+
}
168+
result.ast.templateBody = templateBody
169+
170+
if (options.eslintScopeManager) {
171+
if (scripts.some(isScriptSetupElement)) {
172+
if (!result.scopeManager) {
173+
result.scopeManager = analyzeScope(result.ast, options)
174+
}
175+
analyzeScriptSetupScope(
176+
result.scopeManager,
177+
templateBody,
178+
rootAST,
179+
options,
180+
)
181+
}
182+
}
183+
184+
return {
185+
result,
186+
locationCalculator,
187+
document: rootAST,
188+
}
189+
}
190+
191+
function parseAsScript(code: string, options: ParserOptions) {
192+
return parseScript(code, {
193+
...options,
194+
ecmaVersion: options.ecmaVersion || DEFAULT_ECMA_VERSION,
195+
parser: getScriptParser(options.parser, () => {
196+
const ext = (
197+
path.extname(options.filePath || "unknown.js").toLowerCase() ||
198+
""
199+
)
200+
// remove dot
201+
.slice(1)
202+
if (/^[jt]sx$/u.test(ext)) {
203+
return [ext, ext.slice(0, -1)]
204+
}
205+
206+
return ext
207+
}),
208+
})
209+
}

0 commit comments

Comments
 (0)