Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: intlify/eslint-plugin-vue-i18n
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v3.0.0-next.9
Choose a base ref
...
head repository: intlify/eslint-plugin-vue-i18n
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v3.0.0-next.10
Choose a head ref
  • 3 commits
  • 21 files changed
  • 4 contributors

Commits on Apr 11, 2024

  1. chore: update rule-proposal.md

    kazupon authored Apr 11, 2024
    Copy the full SHA
    e827a23 View commit details

Commits on Apr 13, 2024

  1. 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
    ota-meshi authored Apr 13, 2024
    Copy the full SHA
    c392a38 View commit details
  2. chore: release @intlify/eslint-plugin-vue-i18n (next) (#498)

    Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
    github-actions[bot] and github-actions[bot] authored Apr 13, 2024
    Copy the full SHA
    e325ab2 View commit details
3 changes: 2 additions & 1 deletion .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@
"olive-chairs-invent",
"pink-hairs-fail",
"shiny-colts-search",
"ten-insects-deny"
"ten-insects-deny",
"wicked-carpets-sing"
]
}
5 changes: 5 additions & 0 deletions .changeset/wicked-carpets-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@intlify/eslint-plugin-vue-i18n": minor
---

fix: `no-unused-keys` rule not working when using flat config
8 changes: 4 additions & 4 deletions .github/ISSUE_TEMPLATE/rule-proposal.md
Original file line number Diff line number Diff line change
@@ -16,10 +16,10 @@ about: Suggest an idea for a new rule
**What category should the rule belong to?**
<!-- (place an "X" next to just one item) -->

[ ] Enforces code style (layout)
[ ] Warns about a potential error (problem)
[ ] Suggests an alternate way of doing something (suggestion)
[ ] Other (please specify:)
- [ ] Enforces code style (layout)
- [ ] Warns about a potential error (problem)
- [ ] Suggests an alternate way of doing something (suggestion)
- [ ] Other (please specify:)

**Provide 2-3 code examples that this rule should warn about:**

6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @intlify/eslint-plugin-vue-i18n

## 3.0.0-next.10

### Minor Changes

- [#497](https://github.com/intlify/eslint-plugin-vue-i18n/pull/497) [`c392a38`](https://github.com/intlify/eslint-plugin-vue-i18n/commit/c392a38591c19b406453df69e82225b8fc9ca076) Thanks [@ota-meshi](https://github.com/ota-meshi)! - fix: `no-unused-keys` rule not working when using flat config

## 3.0.0-next.9

### Minor Changes
1 change: 0 additions & 1 deletion files/empty.json

This file was deleted.

82 changes: 10 additions & 72 deletions lib/utils/collect-keys.ts
Original file line number Diff line number Diff line change
@@ -2,23 +2,19 @@
* @fileoverview Collect localization keys
* @author kazuya kawaguchi (a.k.a. kazupon)
*/
import type { Linter } from 'eslint'
import { parseForESLint, AST as VAST } from 'vue-eslint-parser'
import { readFileSync } from 'fs'
import { AST as VAST } from 'vue-eslint-parser'
import { resolve, extname } from 'path'
import { listFilesToProcess } from './glob-utils'
import { ResourceLoader } from './resource-loader'
import { CacheLoader } from './cache-loader'
import { defineCacheFunction } from './cache-function'
import debugBuilder from 'debug'
import type { RuleContext, VisitorKeys } from '../types'
// @ts-expect-error -- ignore
import { Legacy } from '@eslint/eslintrc'
import { getCwd } from './get-cwd'
import { isStaticLiteral, getStaticLiteralValue } from './index'
import importFresh from 'import-fresh'
import type { Parser } from './parser-config-resolver'
import { buildParserFromConfig } from './parser-config-resolver'
const debug = debugBuilder('eslint-plugin-vue-i18n:collect-keys')
const { CascadingConfigArrayFactory } = Legacy

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

function getParser(parser: string | undefined): {
parseForESLint?: typeof parseForESLint
parse: (code: string, options: unknown) => VAST.ESLintProgram
} {
if (parser) {
try {
return require(parser)
} catch (_e) {
// ignore
}
}
return {
parseForESLint,
parse(code: string, options: unknown) {
return parseForESLint(code, options).ast
}
}
}

/**
* Collect the used keys from source code text.
* @param {string} text
* @param {string} filename
* @returns {string[]}
*/
function collectKeysFromText(
text: string,
filename: string,
getConfigForFile: (filePath: string) => Linter.Config<Linter.RulesRecord>
) {
function collectKeysFromText(filename: string, parser: Parser) {
const effectiveFilename = filename || '<text>'
debug(`collectKeysFromFile ${effectiveFilename}`)
const config = getConfigForFile(effectiveFilename)
const parser = getParser(config.parser)

const parserOptions = Object.assign({}, config.parserOptions, {
loc: true,
range: true,
raw: true,
tokens: true,
comment: true,
eslintVisitorKeys: true,
eslintScopeManager: true,
filePath: effectiveFilename
})
try {
const parseResult =
typeof parser.parseForESLint === 'function'
? parser.parseForESLint(text, parserOptions)
: { ast: parser.parse(text, parserOptions) }
const parseResult = parser(filename)
if (!parseResult) {
return []
}
return collectKeysFromAST(parseResult.ast, parseResult.visitorKeys)
} catch (_e) {
return []
@@ -137,20 +97,7 @@ function collectKeysFromText(
function collectKeyResourcesFromFiles(fileNames: string[], cwd: string) {
debug('collectKeysFromFiles', fileNames)

const configArrayFactory = new CascadingConfigArrayFactory({
additionalPluginPool: new Map([
['@intlify/vue-i18n', importFresh('../index')]
]),
cwd,
async getEslintRecommendedConfig() {
return await import('../../files/empty.json')
},
async getEslintAllConfig() {
return await import('../../files/empty.json')
},
eslintRecommendedPath: require.resolve('../../files/empty.json'),
eslintAllPath: require.resolve('../../files/empty.json')
})
const parser = buildParserFromConfig(cwd)

const results = []

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

results.push(
new ResourceLoader(resolve(filename), () => {
const text = readFileSync(resolve(filename), 'utf8')
return collectKeysFromText(text, filename, getConfigForFile)
return collectKeysFromText(filename, parser)
})
)
}

return results

function getConfigForFile(filePath: string) {
const absolutePath = resolve(cwd, filePath)
return configArrayFactory
.getConfigArrayForFile(absolutePath)
.extractConfig(absolutePath)
.toCompatibleObjectAsConfigFileContent()
}
}

/**
14 changes: 14 additions & 0 deletions lib/utils/parser-config-resolver/build-parser-using-flat-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// @ts-expect-error -- ignore
import { createSyncFn } from 'synckit'
import type { ParseResult, Parser } from '.'

const getSync = createSyncFn(require.resolve('./worker'))

/**
* Build synchronously parser using the flat config
*/
export function buildParserUsingFlatConfig(cwd: string): Parser {
return (filePath: string) => {
return getSync(cwd, filePath) as ParseResult
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Parser } from '.'
// @ts-expect-error -- ignore
import { Legacy } from '@eslint/eslintrc'
import path from 'path'
import { parseByParser } from './parse-by-parser'
const { CascadingConfigArrayFactory } = Legacy

/**
* Build parser using legacy config
*/
export function buildParserUsingLegacyConfig(cwd: string): Parser {
const configArrayFactory = new CascadingConfigArrayFactory({
additionalPluginPool: new Map([
['@intlify/vue-i18n', require('../../index')]
]),
cwd,
getEslintRecommendedConfig() {
return {}
},
getEslintAllConfig() {
return {}
}
})

function getConfigForFile(filePath: string) {
const absolutePath = path.resolve(cwd, filePath)
return configArrayFactory
.getConfigArrayForFile(absolutePath)
.extractConfig(absolutePath)
.toCompatibleObjectAsConfigFileContent()
}

return (filePath: string) => {
const config = getConfigForFile(filePath)

const parserOptions = Object.assign({}, config.parserOptions, {
loc: true,
range: true,
raw: true,
tokens: true,
comment: true,
eslintVisitorKeys: true,
eslintScopeManager: true,
filePath
})
return parseByParser(filePath, config.parser, parserOptions)
}
}
24 changes: 24 additions & 0 deletions lib/utils/parser-config-resolver/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { shouldUseFlatConfig } from './should-use-flat-config'
import type { AST as VAST } from 'vue-eslint-parser'
import { buildParserUsingLegacyConfig } from './build-parser-using-legacy-config'
import { buildParserUsingFlatConfig } from './build-parser-using-flat-config'

export type ParseResult = Pick<
VAST.ESLintExtendedProgram,
'ast' | 'visitorKeys'
> | null
export type Parser = (filePath: string) => ParseResult

const parsers: Record<string, undefined | Parser> = {}

export function buildParserFromConfig(cwd: string): Parser {
const parser = parsers[cwd]
if (parser) {
return parser
}
if (shouldUseFlatConfig(cwd)) {
return (parsers[cwd] = buildParserUsingFlatConfig(cwd))
}

return (parsers[cwd] = buildParserUsingLegacyConfig(cwd))
}
45 changes: 45 additions & 0 deletions lib/utils/parser-config-resolver/parse-by-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Linter } from 'eslint'
import { readFileSync } from 'fs'
import path from 'path'
import { parseForESLint } from 'vue-eslint-parser'
import type { ParseResult } from '.'

export function parseByParser(
filePath: string,
parserDefine: Linter.ParserModule | string | undefined,
parserOptions: unknown
): ParseResult {
const parser = getParser(parserDefine, filePath)
try {
const text = readFileSync(path.resolve(filePath), 'utf8')
const parseResult =
'parseForESLint' in parser && typeof parser.parseForESLint === 'function'
? parser.parseForESLint(text, parserOptions)
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
{ ast: (parser as any).parse(text, parserOptions) }
return parseResult as ParseResult
} catch (_e) {
return null
}
}

function getParser(
parser: Linter.ParserModule | string | undefined,
filePath: string
): Linter.ParserModule {
if (parser) {
if (typeof parser === 'string') {
try {
return require(parser)
} catch (_e) {
// ignore
}
} else {
return parser
}
}
if (filePath.endsWith('.vue')) {
return { parseForESLint } as Linter.ParserModule
}
return require('espree')
}
66 changes: 66 additions & 0 deletions lib/utils/parser-config-resolver/should-use-flat-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/** copied from https://github.com/eslint/eslint/blob/v8.56.0/lib/eslint/flat-eslint.js#L1119 */

import path from 'path'
import fs from 'fs'

const FLAT_CONFIG_FILENAMES = [
'eslint.config.js',
'eslint.config.mjs',
'eslint.config.cjs'
]
/**
* Returns whether flat config should be used.
* @returns {Promise<boolean>} Whether flat config should be used.
*/
export function shouldUseFlatConfig(cwd: string): boolean {
// eslint-disable-next-line no-process-env -- ignore
switch (process.env.ESLINT_USE_FLAT_CONFIG) {
case 'true':
return true
case 'false':
return false
default:
// If neither explicitly enabled nor disabled, then use the presence
// of a flat config file to determine enablement.
return Boolean(findFlatConfigFile(cwd))
}
}

/**
* Searches from the current working directory up until finding the
* given flat config filename.
* @param {string} cwd The current working directory to search from.
* @returns {string|undefined} The filename if found or `undefined` if not.
*/
export function findFlatConfigFile(cwd: string) {
return findUp(FLAT_CONFIG_FILENAMES, { cwd })
}

/** We used https://github.com/sindresorhus/find-up/blob/b733bb70d3aa21b22fa011be8089110d467c317f/index.js#L94 as a reference */
function findUp(names: string[], options: { cwd: string }) {
let directory = path.resolve(options.cwd)
const { root } = path.parse(directory)
const stopAt = path.resolve(directory, root)
// eslint-disable-next-line no-constant-condition -- ignore
while (true) {
for (const name of names) {
const target = path.resolve(directory, name)
const stat = fs.existsSync(target)
? fs.statSync(target, {
throwIfNoEntry: false
})
: null
if (stat?.isFile()) {
return target
}
}

if (directory === stopAt) {
break
}

directory = path.dirname(directory)
}

return null
}
Loading