Skip to content

Commit c392a38

Browse files
authored
fix: no-unused-keys rule not working when using flat config (#497)
* fix: `no-unused-keys` rule not working when using flat config * fix * Create wicked-carpets-sing.md * test * fix * fix * fix
1 parent e827a23 commit c392a38

File tree

18 files changed

+1369
-529
lines changed

18 files changed

+1369
-529
lines changed

.changeset/wicked-carpets-sing.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@intlify/eslint-plugin-vue-i18n": minor
3+
---
4+
5+
fix: `no-unused-keys` rule not working when using flat config

files/empty.json

-1
This file was deleted.

lib/utils/collect-keys.ts

+10-72
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,19 @@
22
* @fileoverview Collect localization keys
33
* @author kazuya kawaguchi (a.k.a. kazupon)
44
*/
5-
import type { Linter } from 'eslint'
6-
import { parseForESLint, AST as VAST } from 'vue-eslint-parser'
7-
import { readFileSync } from 'fs'
5+
import { AST as VAST } from 'vue-eslint-parser'
86
import { resolve, extname } from 'path'
97
import { listFilesToProcess } from './glob-utils'
108
import { ResourceLoader } from './resource-loader'
119
import { CacheLoader } from './cache-loader'
1210
import { defineCacheFunction } from './cache-function'
1311
import debugBuilder from 'debug'
1412
import type { RuleContext, VisitorKeys } from '../types'
15-
// @ts-expect-error -- ignore
16-
import { Legacy } from '@eslint/eslintrc'
1713
import { getCwd } from './get-cwd'
1814
import { isStaticLiteral, getStaticLiteralValue } from './index'
19-
import importFresh from 'import-fresh'
15+
import type { Parser } from './parser-config-resolver'
16+
import { buildParserFromConfig } from './parser-config-resolver'
2017
const debug = debugBuilder('eslint-plugin-vue-i18n:collect-keys')
21-
const { CascadingConfigArrayFactory } = Legacy
2218

2319
/**
2420
*
@@ -74,56 +70,20 @@ function getKeyFromI18nComponent(node: VAST.VAttribute) {
7470
}
7571
}
7672

77-
function getParser(parser: string | undefined): {
78-
parseForESLint?: typeof parseForESLint
79-
parse: (code: string, options: unknown) => VAST.ESLintProgram
80-
} {
81-
if (parser) {
82-
try {
83-
return require(parser)
84-
} catch (_e) {
85-
// ignore
86-
}
87-
}
88-
return {
89-
parseForESLint,
90-
parse(code: string, options: unknown) {
91-
return parseForESLint(code, options).ast
92-
}
93-
}
94-
}
95-
9673
/**
9774
* Collect the used keys from source code text.
9875
* @param {string} text
9976
* @param {string} filename
10077
* @returns {string[]}
10178
*/
102-
function collectKeysFromText(
103-
text: string,
104-
filename: string,
105-
getConfigForFile: (filePath: string) => Linter.Config<Linter.RulesRecord>
106-
) {
79+
function collectKeysFromText(filename: string, parser: Parser) {
10780
const effectiveFilename = filename || '<text>'
10881
debug(`collectKeysFromFile ${effectiveFilename}`)
109-
const config = getConfigForFile(effectiveFilename)
110-
const parser = getParser(config.parser)
111-
112-
const parserOptions = Object.assign({}, config.parserOptions, {
113-
loc: true,
114-
range: true,
115-
raw: true,
116-
tokens: true,
117-
comment: true,
118-
eslintVisitorKeys: true,
119-
eslintScopeManager: true,
120-
filePath: effectiveFilename
121-
})
12282
try {
123-
const parseResult =
124-
typeof parser.parseForESLint === 'function'
125-
? parser.parseForESLint(text, parserOptions)
126-
: { ast: parser.parse(text, parserOptions) }
83+
const parseResult = parser(filename)
84+
if (!parseResult) {
85+
return []
86+
}
12787
return collectKeysFromAST(parseResult.ast, parseResult.visitorKeys)
12888
} catch (_e) {
12989
return []
@@ -137,20 +97,7 @@ function collectKeysFromText(
13797
function collectKeyResourcesFromFiles(fileNames: string[], cwd: string) {
13898
debug('collectKeysFromFiles', fileNames)
13999

140-
const configArrayFactory = new CascadingConfigArrayFactory({
141-
additionalPluginPool: new Map([
142-
['@intlify/vue-i18n', importFresh('../index')]
143-
]),
144-
cwd,
145-
async getEslintRecommendedConfig() {
146-
return await import('../../files/empty.json')
147-
},
148-
async getEslintAllConfig() {
149-
return await import('../../files/empty.json')
150-
},
151-
eslintRecommendedPath: require.resolve('../../files/empty.json'),
152-
eslintAllPath: require.resolve('../../files/empty.json')
153-
})
100+
const parser = buildParserFromConfig(cwd)
154101

155102
const results = []
156103

@@ -160,21 +107,12 @@ function collectKeyResourcesFromFiles(fileNames: string[], cwd: string) {
160107

161108
results.push(
162109
new ResourceLoader(resolve(filename), () => {
163-
const text = readFileSync(resolve(filename), 'utf8')
164-
return collectKeysFromText(text, filename, getConfigForFile)
110+
return collectKeysFromText(filename, parser)
165111
})
166112
)
167113
}
168114

169115
return results
170-
171-
function getConfigForFile(filePath: string) {
172-
const absolutePath = resolve(cwd, filePath)
173-
return configArrayFactory
174-
.getConfigArrayForFile(absolutePath)
175-
.extractConfig(absolutePath)
176-
.toCompatibleObjectAsConfigFileContent()
177-
}
178116
}
179117

180118
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// @ts-expect-error -- ignore
2+
import { createSyncFn } from 'synckit'
3+
import type { ParseResult, Parser } from '.'
4+
5+
const getSync = createSyncFn(require.resolve('./worker'))
6+
7+
/**
8+
* Build synchronously parser using the flat config
9+
*/
10+
export function buildParserUsingFlatConfig(cwd: string): Parser {
11+
return (filePath: string) => {
12+
return getSync(cwd, filePath) as ParseResult
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { Parser } from '.'
2+
// @ts-expect-error -- ignore
3+
import { Legacy } from '@eslint/eslintrc'
4+
import path from 'path'
5+
import { parseByParser } from './parse-by-parser'
6+
const { CascadingConfigArrayFactory } = Legacy
7+
8+
/**
9+
* Build parser using legacy config
10+
*/
11+
export function buildParserUsingLegacyConfig(cwd: string): Parser {
12+
const configArrayFactory = new CascadingConfigArrayFactory({
13+
additionalPluginPool: new Map([
14+
['@intlify/vue-i18n', require('../../index')]
15+
]),
16+
cwd,
17+
getEslintRecommendedConfig() {
18+
return {}
19+
},
20+
getEslintAllConfig() {
21+
return {}
22+
}
23+
})
24+
25+
function getConfigForFile(filePath: string) {
26+
const absolutePath = path.resolve(cwd, filePath)
27+
return configArrayFactory
28+
.getConfigArrayForFile(absolutePath)
29+
.extractConfig(absolutePath)
30+
.toCompatibleObjectAsConfigFileContent()
31+
}
32+
33+
return (filePath: string) => {
34+
const config = getConfigForFile(filePath)
35+
36+
const parserOptions = Object.assign({}, config.parserOptions, {
37+
loc: true,
38+
range: true,
39+
raw: true,
40+
tokens: true,
41+
comment: true,
42+
eslintVisitorKeys: true,
43+
eslintScopeManager: true,
44+
filePath
45+
})
46+
return parseByParser(filePath, config.parser, parserOptions)
47+
}
48+
}
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { shouldUseFlatConfig } from './should-use-flat-config'
2+
import type { AST as VAST } from 'vue-eslint-parser'
3+
import { buildParserUsingLegacyConfig } from './build-parser-using-legacy-config'
4+
import { buildParserUsingFlatConfig } from './build-parser-using-flat-config'
5+
6+
export type ParseResult = Pick<
7+
VAST.ESLintExtendedProgram,
8+
'ast' | 'visitorKeys'
9+
> | null
10+
export type Parser = (filePath: string) => ParseResult
11+
12+
const parsers: Record<string, undefined | Parser> = {}
13+
14+
export function buildParserFromConfig(cwd: string): Parser {
15+
const parser = parsers[cwd]
16+
if (parser) {
17+
return parser
18+
}
19+
if (shouldUseFlatConfig(cwd)) {
20+
return (parsers[cwd] = buildParserUsingFlatConfig(cwd))
21+
}
22+
23+
return (parsers[cwd] = buildParserUsingLegacyConfig(cwd))
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Linter } from 'eslint'
2+
import { readFileSync } from 'fs'
3+
import path from 'path'
4+
import { parseForESLint } from 'vue-eslint-parser'
5+
import type { ParseResult } from '.'
6+
7+
export function parseByParser(
8+
filePath: string,
9+
parserDefine: Linter.ParserModule | string | undefined,
10+
parserOptions: unknown
11+
): ParseResult {
12+
const parser = getParser(parserDefine, filePath)
13+
try {
14+
const text = readFileSync(path.resolve(filePath), 'utf8')
15+
const parseResult =
16+
'parseForESLint' in parser && typeof parser.parseForESLint === 'function'
17+
? parser.parseForESLint(text, parserOptions)
18+
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
19+
{ ast: (parser as any).parse(text, parserOptions) }
20+
return parseResult as ParseResult
21+
} catch (_e) {
22+
return null
23+
}
24+
}
25+
26+
function getParser(
27+
parser: Linter.ParserModule | string | undefined,
28+
filePath: string
29+
): Linter.ParserModule {
30+
if (parser) {
31+
if (typeof parser === 'string') {
32+
try {
33+
return require(parser)
34+
} catch (_e) {
35+
// ignore
36+
}
37+
} else {
38+
return parser
39+
}
40+
}
41+
if (filePath.endsWith('.vue')) {
42+
return { parseForESLint } as Linter.ParserModule
43+
}
44+
return require('espree')
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/** copied from https://github.com/eslint/eslint/blob/v8.56.0/lib/eslint/flat-eslint.js#L1119 */
2+
3+
import path from 'path'
4+
import fs from 'fs'
5+
6+
const FLAT_CONFIG_FILENAMES = [
7+
'eslint.config.js',
8+
'eslint.config.mjs',
9+
'eslint.config.cjs'
10+
]
11+
/**
12+
* Returns whether flat config should be used.
13+
* @returns {Promise<boolean>} Whether flat config should be used.
14+
*/
15+
export function shouldUseFlatConfig(cwd: string): boolean {
16+
// eslint-disable-next-line no-process-env -- ignore
17+
switch (process.env.ESLINT_USE_FLAT_CONFIG) {
18+
case 'true':
19+
return true
20+
case 'false':
21+
return false
22+
default:
23+
// If neither explicitly enabled nor disabled, then use the presence
24+
// of a flat config file to determine enablement.
25+
return Boolean(findFlatConfigFile(cwd))
26+
}
27+
}
28+
29+
/**
30+
* Searches from the current working directory up until finding the
31+
* given flat config filename.
32+
* @param {string} cwd The current working directory to search from.
33+
* @returns {string|undefined} The filename if found or `undefined` if not.
34+
*/
35+
export function findFlatConfigFile(cwd: string) {
36+
return findUp(FLAT_CONFIG_FILENAMES, { cwd })
37+
}
38+
39+
/** We used https://github.com/sindresorhus/find-up/blob/b733bb70d3aa21b22fa011be8089110d467c317f/index.js#L94 as a reference */
40+
function findUp(names: string[], options: { cwd: string }) {
41+
let directory = path.resolve(options.cwd)
42+
const { root } = path.parse(directory)
43+
const stopAt = path.resolve(directory, root)
44+
// eslint-disable-next-line no-constant-condition -- ignore
45+
while (true) {
46+
for (const name of names) {
47+
const target = path.resolve(directory, name)
48+
const stat = fs.existsSync(target)
49+
? fs.statSync(target, {
50+
throwIfNoEntry: false
51+
})
52+
: null
53+
if (stat?.isFile()) {
54+
return target
55+
}
56+
}
57+
58+
if (directory === stopAt) {
59+
break
60+
}
61+
62+
directory = path.dirname(directory)
63+
}
64+
65+
return null
66+
}
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// @ts-expect-error -- ignore
2+
import { runAsWorker } from 'synckit'
3+
import { getESLint } from 'eslint-compat-utils/eslint'
4+
import type { Linter } from 'eslint'
5+
import type { ParseResult } from '.'
6+
import { parseByParser } from './parse-by-parser'
7+
const ESLint = getESLint()
8+
9+
runAsWorker(async (cwd: string, filePath: string): Promise<ParseResult> => {
10+
const eslint = new ESLint({ cwd })
11+
const config: Linter.FlatConfig = await eslint.calculateConfigForFile(
12+
filePath
13+
)
14+
const languageOptions = config.languageOptions || {}
15+
const parserOptions = Object.assign(
16+
{
17+
sourceType: languageOptions.sourceType || 'module',
18+
ecmaVersion: languageOptions.ecmaVersion || 'latest'
19+
},
20+
languageOptions.parserOptions,
21+
{
22+
loc: true,
23+
range: true,
24+
raw: true,
25+
tokens: true,
26+
comment: true,
27+
eslintVisitorKeys: true,
28+
eslintScopeManager: true,
29+
filePath
30+
}
31+
)
32+
33+
const result = parseByParser(filePath, languageOptions.parser, parserOptions)
34+
if (!result) {
35+
return null
36+
}
37+
38+
return {
39+
ast: result.ast,
40+
visitorKeys: result?.visitorKeys
41+
}
42+
})

0 commit comments

Comments
 (0)