diff --git a/lib/rules/key-format-style.ts b/lib/rules/key-format-style.ts index 6150ed71..01d8fc79 100644 --- a/lib/rules/key-format-style.ts +++ b/lib/rules/key-format-style.ts @@ -1,21 +1,18 @@ /** * @author Yosuke Ota */ -import type { AST as VAST } from 'vue-eslint-parser' import type { AST as JSONAST } from 'jsonc-eslint-parser' import type { AST as YAMLAST } from 'yaml-eslint-parser' import { extname } from 'path' -import { getLocaleMessages } from '../utils/index' +import { defineCustomBlocksVisitor, getLocaleMessages } from '../utils/index' import debugBuilder from 'debug' import type { RuleContext, RuleListener } from '../types' import { getCasingChecker } from '../utils/casing' -import { LocaleMessage } from '../utils/locale-messages' +import type { LocaleMessage } from '../utils/locale-messages' const debug = debugBuilder('eslint-plugin-vue-i18n:key-format-style') const allowedCaseOptions = ['camelCase', 'kebab-case', 'snake_case'] as const type CaseOption = typeof allowedCaseOptions[number] -const unknownKey = Symbol('unknown key') -type UnknownKey = typeof unknownKey function create(context: RuleContext): RuleListener { const filename = context.getFilename() @@ -23,38 +20,48 @@ function create(context: RuleContext): RuleListener { const checker = getCasingChecker(expectCasing) const allowArray: boolean = context.options[1]?.allowArray + function reportUnknown(reportNode: YAMLAST.YAMLNode) { + context.report({ + message: `Unexpected object key. Use ${expectCasing} string key instead`, + loc: reportNode.loc + }) + } + function verifyKey( + key: string | number, + reportNode: JSONAST.JSONNode | YAMLAST.YAMLNode + ) { + if (typeof key === 'number') { + if (!allowArray) { + context.report({ + message: `Unexpected array element`, + loc: reportNode.loc + }) + } + } else { + if (!checker(key)) { + context.report({ + message: `"${key}" is not ${expectCasing}`, + loc: reportNode.loc + }) + } + } + } /** - * Create node visitor + * Create node visitor for JSON */ - function createVisitor( - targetLocaleMessage: LocaleMessage, - { - skipNode, - resolveKey, - resolveReportNode - }: { - skipNode: (node: N) => boolean - resolveKey: (node: N) => string | number | UnknownKey | null - resolveReportNode: (node: N) => N - } - ) { + function createVisitorForJson( + targetLocaleMessage: LocaleMessage + ): RuleListener { type KeyStack = { inLocale: boolean - node?: N + node?: JSONAST.JSONNode upper?: KeyStack } let keyStack: KeyStack = { inLocale: targetLocaleMessage.localeKey === 'file' } return { - enterNode(node: N) { - if (skipNode(node)) { - return - } - const key = resolveKey(node) - if (key == null) { - return - } + JSONProperty(node: JSONAST.JSONProperty) { const { inLocale } = keyStack keyStack = { node, @@ -64,182 +71,122 @@ function create(context: RuleContext): RuleListener { if (!inLocale) { return } - if (key === unknownKey) { - context.report({ - message: `Unexpected object key. Use ${expectCasing} string key instead`, - loc: resolveReportNode(node).loc - }) - } else if (typeof key === 'number') { - if (!allowArray) { - context.report({ - message: `Unexpected array element`, - loc: resolveReportNode(node).loc - }) - } - } else { - if (!checker(key)) { - context.report({ - message: `"${key}" is not ${expectCasing}`, - loc: resolveReportNode(node).loc - }) - } - } + + const key = + node.key.type === 'JSONLiteral' ? `${node.key.value}` : node.key.name + + verifyKey(key, node.key) }, - leaveNode(node: N) { - if (keyStack.node === node) { - keyStack = keyStack.upper! - } - } - } - } - /** - * Create node visitor for JSON - */ - function createVisitorForJson(targetLocaleMessage: LocaleMessage) { - return createVisitor(targetLocaleMessage, { - skipNode(node) { - if ( - node.type === 'Program' || - node.type === 'JSONExpressionStatement' || - node.type === 'JSONProperty' - ) { - return true - } - const parent = node.parent! - if (parent.type === 'JSONProperty' && parent.key === node) { - return true - } - return false + 'JSONProperty:exit'() { + keyStack = keyStack.upper! }, - resolveKey(node) { - const parent = node.parent! - if (parent.type === 'JSONProperty') { - return parent.key.type === 'JSONLiteral' - ? `${parent.key.value}` - : parent.key.name - } else if (parent.type === 'JSONArrayExpression') { - return parent.elements.indexOf(node as never) + 'JSONArrayExpression > *'( + node: JSONAST.JSONArrayExpression['elements'][number] & { + parent: JSONAST.JSONArrayExpression } - return null - }, - resolveReportNode(node) { - const parent = node.parent! - return parent.type === 'JSONProperty' ? parent.key : node + ) { + const key = node.parent.elements.indexOf(node) + verifyKey(key, node) } - }) + } } /** * Create node visitor for YAML */ - function createVisitorForYaml(targetLocaleMessage: LocaleMessage) { - const yamlKeyNodes = new Set() - return createVisitor(targetLocaleMessage, { - skipNode(node) { + function createVisitorForYaml( + targetLocaleMessage: LocaleMessage + ): RuleListener { + const yamlKeyNodes = new Set() + + type KeyStack = { + inLocale: boolean + node?: YAMLAST.YAMLNode + upper?: KeyStack + } + let keyStack: KeyStack = { + inLocale: targetLocaleMessage.localeKey === 'file' + } + function withinKey(node: YAMLAST.YAMLNode) { + for (const keyNode of yamlKeyNodes) { if ( - node.type === 'Program' || - node.type === 'YAMLDocument' || - node.type === 'YAMLDirective' || - node.type === 'YAMLAnchor' || - node.type === 'YAMLTag' + keyNode.range[0] <= node.range[0] && + node.range[0] < keyNode.range[1] ) { return true } - - if (yamlKeyNodes.has(node)) { - // within key node - return true - } - const parent = node.parent - if (yamlKeyNodes.has(parent)) { - // within key node - yamlKeyNodes.add(node) - return true + } + return false + } + return { + YAMLPair(node: YAMLAST.YAMLPair) { + const { inLocale } = keyStack + keyStack = { + node, + inLocale: true, + upper: keyStack } - if (node.type === 'YAMLPair') { - yamlKeyNodes.add(node.key) - return true + if (!inLocale) { + return } - return false - }, - resolveKey(node) { - const parent = node.parent! - if (parent.type === 'YAMLPair') { - if (parent.key == null) { - return unknownKey - } - if (parent.key.type === 'YAMLScalar') { - const key = parent.key.value - return typeof key === 'string' ? key : String(key) + if (node.key != null) { + if (withinKey(node)) { + return } - return unknownKey - } else if (parent.type === 'YAMLSequence') { - return parent.entries.indexOf(node as never) + yamlKeyNodes.add(node.key) } - return null + if (node.key == null) { + reportUnknown(node) + } else if (node.key.type === 'YAMLScalar') { + const keyValue = node.key.value + const key = typeof keyValue === 'string' ? keyValue : String(keyValue) + verifyKey(key, node.key) + } else { + reportUnknown(node) + } + }, + 'YAMLPair:exit'() { + keyStack = keyStack.upper! }, - resolveReportNode(node) { - const parent = node.parent! - return parent.type === 'YAMLPair' ? parent.key || parent : node + 'YAMLSequence > *'( + node: YAMLAST.YAMLSequence['entries'][number] & { + parent: YAMLAST.YAMLSequence + } + ) { + if (withinKey(node)) { + return + } + const key = node.parent.entries.indexOf(node) + verifyKey(key, node) } - }) + } } if (extname(filename) === '.vue') { - return { - Program() { - const documentFragment = - context.parserServices.getDocumentFragment && - context.parserServices.getDocumentFragment() - /** @type {VElement[]} */ - const i18nBlocks = - (documentFragment && - documentFragment.children.filter( - (node): node is VAST.VElement => - node.type === 'VElement' && node.name === 'i18n' - )) || - [] - if (!i18nBlocks.length) { - return + return defineCustomBlocksVisitor( + context, + ctx => { + const localeMessages = getLocaleMessages(context) + const targetLocaleMessage = localeMessages.findBlockLocaleMessage( + ctx.parserServices.customBlock + ) + if (!targetLocaleMessage) { + return {} } + return createVisitorForJson(targetLocaleMessage) + }, + ctx => { const localeMessages = getLocaleMessages(context) - - for (const block of i18nBlocks) { - if ( - block.startTag.attributes.some( - attr => !attr.directive && attr.key.name === 'src' - ) - ) { - continue - } - - const targetLocaleMessage = localeMessages.findBlockLocaleMessage( - block - ) - if (!targetLocaleMessage) { - continue - } - const parserLang = targetLocaleMessage.getParserLang() - - let visitor - if (parserLang === 'json') { - visitor = createVisitorForJson(targetLocaleMessage) - } else if (parserLang === 'yaml') { - visitor = createVisitorForYaml(targetLocaleMessage) - } - - if (visitor == null) { - return - } - - targetLocaleMessage.traverseNodes({ - enterNode: visitor.enterNode, - leaveNode: visitor.leaveNode - }) + const targetLocaleMessage = localeMessages.findBlockLocaleMessage( + ctx.parserServices.customBlock + ) + if (!targetLocaleMessage) { + return {} } + return createVisitorForYaml(targetLocaleMessage) } - } + ) } else if (context.parserServices.isJSON || context.parserServices.isYAML) { const localeMessages = getLocaleMessages(context) const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename) @@ -249,19 +196,9 @@ function create(context: RuleContext): RuleListener { } if (context.parserServices.isJSON) { - const { enterNode, leaveNode } = createVisitorForJson(targetLocaleMessage) - - return { - '[type=/^JSON/]': enterNode, - '[type=/^JSON/]:exit': leaveNode - } + return createVisitorForJson(targetLocaleMessage) } else if (context.parserServices.isYAML) { - const { enterNode, leaveNode } = createVisitorForYaml(targetLocaleMessage) - - return { - '[type=/^YAML/]': enterNode, - '[type=/^YAML/]:exit': leaveNode - } + return createVisitorForYaml(targetLocaleMessage) } return {} } else { diff --git a/lib/rules/no-duplicate-keys-in-locale.ts b/lib/rules/no-duplicate-keys-in-locale.ts index 876ea90f..de2e0500 100644 --- a/lib/rules/no-duplicate-keys-in-locale.ts +++ b/lib/rules/no-duplicate-keys-in-locale.ts @@ -1,11 +1,10 @@ /** * @author Yosuke Ota */ -import type { AST as VAST } from 'vue-eslint-parser' import type { AST as JSONAST } from 'jsonc-eslint-parser' import type { AST as YAMLAST } from 'yaml-eslint-parser' import { extname } from 'path' -import { getLocaleMessages } from '../utils/index' +import { defineCustomBlocksVisitor, getLocaleMessages } from '../utils/index' import debugBuilder from 'debug' import type { LocaleMessage } from '../utils/locale-messages' import type { @@ -42,73 +41,72 @@ function create(context: RuleContext): RuleListener { const options = (context.options && context.options[0]) || {} const ignoreI18nBlock = Boolean(options.ignoreI18nBlock) - /** - * Create node visitor - */ - function createVisitor( + function createInitPathStack( targetLocaleMessage: LocaleMessage, - otherLocaleMessages: LocaleMessage[], - { - skipNode, - resolveKey, - resolveReportNode - }: { - skipNode: (node: N) => boolean - resolveKey: (node: N) => string | number | null - resolveReportNode: (node: N) => N - } - ) { - let pathStack: PathStack + otherLocaleMessages: LocaleMessage[] + ): PathStack { if (targetLocaleMessage.localeKey === 'file') { const locale = targetLocaleMessage.locales[0] - pathStack = { - keyPath: '', - locale, - otherDictionaries: otherLocaleMessages.map(lm => { - return { - dict: lm.getMessagesFromLocale(locale), - source: lm - } - }) - } + return createInitLocalePathStack(locale, otherLocaleMessages) } else { - pathStack = { + return { keyPath: '', locale: null, otherDictionaries: [] } } + } + function createInitLocalePathStack( + locale: string, + otherLocaleMessages: LocaleMessage[] + ): PathStack { + return { + keyPath: '', + locale, + otherDictionaries: otherLocaleMessages.map(lm => { + return { + dict: lm.getMessagesFromLocale(locale), + source: lm + } + }) + } + } + + function createVerifyContext( + targetLocaleMessage: LocaleMessage, + otherLocaleMessages: LocaleMessage[] + ) { + let pathStack = createInitPathStack( + targetLocaleMessage, + otherLocaleMessages + ) const existsKeyNodes: { - [locale: string]: { [key: string]: { node: N; reported: boolean }[] } + [locale: string]: { [key: string]: N[] } } = {} const existsLocaleNodes: { - [key: string]: { node: N; reported: boolean }[] + [key: string]: N[] } = {} - return { - enterNode(node: N) { - if (skipNode(node)) { - return - } - const key = resolveKey(node) - if (key == null) { - return - } + function pushKey( + exists: { + [key: string]: (JSONAST.JSONNode | YAMLAST.YAMLNode)[] + }, + key: string, + reportNode: JSONAST.JSONNode | YAMLAST.YAMLNode + ) { + const keyNodes = exists[key] || (exists[key] = []) + keyNodes.push(reportNode) + } + return { + enterKey(key: string | number, reportNode: N) { if (pathStack.locale == null) { // locale is resolved const locale = key as string - verifyDupeKey(existsLocaleNodes, locale, node) + pushKey(existsLocaleNodes, locale, reportNode) pathStack = { upper: pathStack, - node, - keyPath: '', - locale, - otherDictionaries: otherLocaleMessages.map(lm => { - return { - dict: lm.getMessagesFromLocale(locale), - source: lm - } - }) + node: reportNode, + ...createInitLocalePathStack(locale, otherLocaleMessages) } return } @@ -124,7 +122,6 @@ function create(context: RuleContext): RuleListener { const nextOtherDictionaries: DictData[] = [] for (const value of keyOtherValues) { if (typeof value.value === 'string') { - const reportNode = resolveReportNode(node) context.report({ message: `duplicate key '${keyPath}' in '${ pathStack.locale @@ -141,52 +138,47 @@ function create(context: RuleContext): RuleListener { } } - verifyDupeKey( + pushKey( existsKeyNodes[pathStack.locale] || (existsKeyNodes[pathStack.locale] = {}), keyPath, - node + reportNode ) pathStack = { upper: pathStack, - node, + node: reportNode, keyPath, locale: pathStack.locale, otherDictionaries: nextOtherDictionaries } }, - leaveNode(node: N) { + leaveKey(node: N | null) { if (pathStack.node === node) { pathStack = pathStack.upper! } - } - } - - function verifyDupeKey( - exists: { - [key: string]: { node: N; reported: boolean }[] }, - key: string, - node: N - ) { - const keyNodes = exists[key] || (exists[key] = []) - keyNodes.push({ - node, - reported: false - }) - if (keyNodes.length > 1) { - for (const keyNode of keyNodes.filter(e => !e.reported)) { - const reportNode = resolveReportNode(keyNode.node) - context.report({ - message: `duplicate key '${key}'`, - loc: reportNode.loc - }) - keyNode.reported = true + reports() { + for (const localeNodes of [ + existsLocaleNodes, + ...Object.values(existsKeyNodes) + ]) { + for (const key of Object.keys(localeNodes)) { + const keyNodes = localeNodes[key] + if (keyNodes.length > 1) { + for (const keyNode of keyNodes) { + context.report({ + message: `duplicate key '${key}'`, + loc: keyNode.loc + }) + } + } + } } } } } + /** * Create node visitor for JSON */ @@ -195,42 +187,37 @@ function create(context: RuleContext): RuleListener { targetLocaleMessage: LocaleMessage, otherLocaleMessages: LocaleMessage[] ) { - return createVisitor( + const verifyContext = createVerifyContext( targetLocaleMessage, - otherLocaleMessages, - { - skipNode(node) { - if ( - node.type === 'Program' || - node.type === 'JSONExpressionStatement' || - node.type === 'JSONProperty' - ) { - return true - } - const parent = node.parent! - if (parent.type === 'JSONProperty' && parent.key === node) { - return true - } - return false - }, - resolveKey(node) { - const parent = node.parent! - if (parent.type === 'JSONProperty') { - return parent.key.type === 'JSONLiteral' - ? `${parent.key.value}` - : parent.key.name - } else if (parent.type === 'JSONArrayExpression') { - return parent.elements.indexOf(node as never) - } - return null - }, + otherLocaleMessages + ) + return { + JSONProperty(node: JSONAST.JSONProperty) { + const key = + node.key.type === 'JSONLiteral' ? `${node.key.value}` : node.key.name - resolveReportNode(node) { - const parent = node.parent! - return parent.type === 'JSONProperty' ? parent.key : node + verifyContext.enterKey(key, node.key) + }, + 'JSONProperty:exit'(node: JSONAST.JSONProperty) { + verifyContext.leaveKey(node.key) + }, + 'JSONArrayExpression > *'( + node: JSONAST.JSONArrayExpression['elements'][number] & { + parent: JSONAST.JSONArrayExpression } + ) { + const key = node.parent.elements.indexOf(node) + verifyContext.enterKey(key, node) + }, + 'JSONArrayExpression > *:exit'( + node: JSONAST.JSONArrayExpression['elements'][number] + ) { + verifyContext.leaveKey(node!) + }, + 'Program:exit'() { + verifyContext.reports() } - ) + } } /** @@ -241,132 +228,106 @@ function create(context: RuleContext): RuleListener { targetLocaleMessage: LocaleMessage, otherLocaleMessages: LocaleMessage[] ) { - const yamlKeyNodes = new Set() - return createVisitor( + const verifyContext = createVerifyContext( targetLocaleMessage, - otherLocaleMessages, - { - skipNode(node) { - if ( - node.type === 'Program' || - node.type === 'YAMLDocument' || - node.type === 'YAMLDirective' || - node.type === 'YAMLAnchor' || - node.type === 'YAMLTag' - ) { - return true + otherLocaleMessages + ) + const yamlKeyNodes = new Set() + function withinKey(node: YAMLAST.YAMLNode) { + for (const keyNode of yamlKeyNodes) { + if ( + keyNode.range[0] <= node.range[0] && + node.range[0] < keyNode.range[1] + ) { + return true + } + } + return false + } + return { + YAMLPair(node: YAMLAST.YAMLPair) { + if (node.key != null) { + if (withinKey(node)) { + return } + yamlKeyNodes.add(node.key) + } else { + return + } - if (yamlKeyNodes.has(node)) { - // within key node - return true - } - const parent = node.parent - if (yamlKeyNodes.has(parent)) { - // within key node - yamlKeyNodes.add(node) - return true - } - if (node.type === 'YAMLPair') { - yamlKeyNodes.add(node.key) - return true - } - return false - }, - resolveKey(node) { - const parent = node.parent! - if (parent.type === 'YAMLPair' && parent.key) { - const key = - parent.key.type !== 'YAMLScalar' - ? sourceCode.getText(parent.key) - : parent.key.value - return typeof key === 'boolean' || key === null ? String(key) : key - } else if (parent.type === 'YAMLSequence') { - return parent.entries.indexOf(node as never) - } + const keyValue = + node.key.type !== 'YAMLScalar' + ? sourceCode.getText(node.key) + : node.key.value + const key = + typeof keyValue === 'boolean' || keyValue === null + ? String(keyValue) + : keyValue - return null - }, - resolveReportNode(node) { - const parent = node.parent! - return parent.type === 'YAMLPair' ? parent.key || parent : node + verifyContext.enterKey(key, node.key) + }, + 'YAMLPair:exit'(node: YAMLAST.YAMLPair) { + verifyContext.leaveKey(node.key) + }, + 'YAMLSequence > *'( + node: YAMLAST.YAMLSequence['entries'][number] & { + parent: YAMLAST.YAMLSequence } + ) { + const key = node.parent.entries.indexOf(node) + verifyContext.enterKey(key, node) + }, + 'YAMLSequence > *:exit'(node: YAMLAST.YAMLSequence['entries'][number]) { + verifyContext.leaveKey(node!) + }, + 'Program:exit'() { + verifyContext.reports() } - ) + } } if (extname(filename) === '.vue') { - return { - Program() { - const documentFragment = - context.parserServices.getDocumentFragment && - context.parserServices.getDocumentFragment() - /** @type {VElement[]} */ - const i18nBlocks = - (documentFragment && - documentFragment.children.filter( - (node): node is VAST.VElement => - node.type === 'VElement' && node.name === 'i18n' - )) || - [] - if (!i18nBlocks.length) { - return - } + return defineCustomBlocksVisitor( + context, + ctx => { const localeMessages = getLocaleMessages(context) - - for (const block of i18nBlocks) { - if ( - block.startTag.attributes.some( - attr => !attr.directive && attr.key.name === 'src' - ) - ) { - continue - } - - const targetLocaleMessage = localeMessages.findBlockLocaleMessage( - block - ) - if (!targetLocaleMessage) { - continue - } - const sourceCode = targetLocaleMessage.getSourceCode() - if (!sourceCode) { - continue - } - - const otherLocaleMessages: LocaleMessage[] = ignoreI18nBlock - ? [] - : localeMessages.localeMessages.filter( - lm => lm !== targetLocaleMessage - ) - const parserLang = targetLocaleMessage.getParserLang() - - let visitor - if (parserLang === 'json') { - visitor = createVisitorForJson( - sourceCode, - targetLocaleMessage, - otherLocaleMessages - ) - } else if (parserLang === 'yaml') { - visitor = createVisitorForYaml( - sourceCode, - targetLocaleMessage, - otherLocaleMessages + const targetLocaleMessage = localeMessages.findBlockLocaleMessage( + ctx.parserServices.customBlock + ) + if (!targetLocaleMessage) { + return {} + } + const otherLocaleMessages: LocaleMessage[] = ignoreI18nBlock + ? [] + : localeMessages.localeMessages.filter( + lm => lm !== targetLocaleMessage ) - } - - if (visitor == null) { - return - } - - targetLocaleMessage.traverseNodes({ - enterNode: visitor.enterNode, - leaveNode: visitor.leaveNode - }) + return createVisitorForJson( + ctx.getSourceCode(), + targetLocaleMessage, + otherLocaleMessages + ) + }, + ctx => { + const localeMessages = getLocaleMessages(context) + const targetLocaleMessage = localeMessages.findBlockLocaleMessage( + ctx.parserServices.customBlock + ) + if (!targetLocaleMessage) { + return {} } + const otherLocaleMessages: LocaleMessage[] = ignoreI18nBlock + ? [] + : localeMessages.localeMessages.filter( + lm => lm !== targetLocaleMessage + ) + return createVisitorForYaml( + ctx.getSourceCode(), + targetLocaleMessage, + otherLocaleMessages + ) } - } + ) } else if (context.parserServices.isJSON || context.parserServices.isYAML) { const localeMessages = getLocaleMessages(context) const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename) @@ -381,27 +342,17 @@ function create(context: RuleContext): RuleListener { ) if (context.parserServices.isJSON) { - const { enterNode, leaveNode } = createVisitorForJson( + return createVisitorForJson( sourceCode, targetLocaleMessage, otherLocaleMessages ) - - return { - '[type=/^JSON/]': enterNode, - '[type=/^JSON/]:exit': leaveNode - } } else if (context.parserServices.isYAML) { - const { enterNode, leaveNode } = createVisitorForYaml( + return createVisitorForYaml( sourceCode, targetLocaleMessage, otherLocaleMessages ) - - return { - '[type=/^YAML/]': enterNode, - '[type=/^YAML/]:exit': leaveNode - } } return {} } else { diff --git a/lib/rules/no-html-messages.ts b/lib/rules/no-html-messages.ts index cee6c5e8..0aac14e8 100644 --- a/lib/rules/no-html-messages.ts +++ b/lib/rules/no-html-messages.ts @@ -3,9 +3,8 @@ */ import { extname } from 'path' import parse5 from 'parse5' -import { getLocaleMessages } from '../utils/index' +import { defineCustomBlocksVisitor, getLocaleMessages } from '../utils/index' import debugBuilder from 'debug' -import type { AST as VAST } from 'vue-eslint-parser' import type { AST as JSONAST } from 'jsonc-eslint-parser' import type { AST as YAMLAST } from 'yaml-eslint-parser' import type { RuleContext, RuleListener } from '../types' @@ -86,54 +85,19 @@ function create(context: RuleContext): RuleListener { } if (extname(filename) === '.vue') { - return { - Program() { - const documentFragment = - context.parserServices.getDocumentFragment && - context.parserServices.getDocumentFragment() - /** @type {VElement[]} */ - const i18nBlocks = - (documentFragment && - documentFragment.children.filter( - (node): node is VAST.VElement => - node.type === 'VElement' && node.name === 'i18n' - )) || - [] - if (!i18nBlocks.length) { - return + return defineCustomBlocksVisitor( + context, + () => { + return { + JSONLiteral: verifyJSONLiteral } - const localeMessages = getLocaleMessages(context) - - for (const block of i18nBlocks) { - if ( - block.startTag.attributes.some( - attr => !attr.directive && attr.key.name === 'src' - ) - ) { - continue - } - - const targetLocaleMessage = localeMessages.findBlockLocaleMessage( - block - ) - if (!targetLocaleMessage) { - continue - } - targetLocaleMessage.traverseNodes({ - enterNode(node) { - if (node.type === 'JSONLiteral') { - verifyJSONLiteral(node) - } else if (node.type === 'YAMLScalar') { - verifyYAMLScalar(node) - } - }, - leaveNode() { - // noop - } - }) + }, + () => { + return { + YAMLScalar: verifyYAMLScalar } } - } + ) } else if (context.parserServices.isJSON) { if (!getLocaleMessages(context).findExistLocaleMessage(filename)) { return {} diff --git a/lib/rules/no-unused-keys.ts b/lib/rules/no-unused-keys.ts index f5efe86d..06753a2b 100644 --- a/lib/rules/no-unused-keys.ts +++ b/lib/rules/no-unused-keys.ts @@ -7,7 +7,7 @@ import type { AST as YAMLAST } from 'yaml-eslint-parser' import { extname } from 'path' import { collectKeysFromAST, usedKeysCache } from '../utils/collect-keys' import { collectLinkedKeys } from '../utils/collect-linked-keys' -import { getLocaleMessages } from '../utils/index' +import { defineCustomBlocksVisitor, getLocaleMessages } from '../utils/index' import debugBuilder from 'debug' import type { LocaleMessage } from '../utils/locale-messages' import type { @@ -74,28 +74,12 @@ function create(context: RuleContext): RuleListener { const options = (context.options && context.options[0]) || {} const enableFix = options.enableFix - /** - * Create node visitor - * @param {UsedKeys} usedKeys - * @param {object} option - * @param { (node: JSONNode | YAMLNode) => boolean } option.skipNode - * @param { (parentPath: string, node: JSONNode | YAMLNode) => {path: string, key: string | number} | null } option.resolveKeysForNode - * @param { (node: JSONNode | YAMLNode) => JSONNode | YAMLNode | null } option.resolveReportNode - * @param { (node: JSONNode | YAMLNode) => null | ((fixer: RuleFixer) => null | Fix | Fix[] | IterableIterator) } option.buildFixer - * @param { (node: JSONNode[] | YAMLNode[]) => ((fixer: RuleFixer) => null | Fix | Fix[] | IterableIterator) } option.buildAllFixer - */ - function createVisitor( + function createVerifyContext( usedKeys: UsedKeys, { - skipNode, - resolveKey, - resolveReportNode, buildFixer, buildAllFixer }: { - skipNode: (node: N) => boolean - resolveKey: (node: N) => string | number | null - resolveReportNode: (node: N) => N | null buildFixer: ( node: N ) => @@ -110,22 +94,11 @@ function create(context: RuleContext): RuleListener { let pathStack: PathStack = { usedKeys, keyPath: '' } const reports: { node: N; keyPath: string }[] = [] return { - /** - * @param {JSONNode | YAMLNode} node - */ - enterNode(node: N) { - if (skipNode(node)) { - return - } - - const key = resolveKey(node) - if (key == null) { - return - } + enterKey(key: string | number, reportNode: N, ignoreReport: boolean) { const keyPath = joinPath(pathStack.keyPath, key) pathStack = { upper: pathStack, - node, + node: reportNode, usedKeys: (pathStack.usedKeys && (pathStack.usedKeys[key] as UsedKeys)) || false, @@ -133,22 +106,18 @@ function create(context: RuleContext): RuleListener { } const isUnused = !pathStack.usedKeys if (isUnused) { - const reportNode = resolveReportNode(node) - if (reportNode == null) { - // ignore - return - } - reports.push({ - node: reportNode, - keyPath - }) + if (!ignoreReport) + reports.push({ + node: reportNode, + keyPath + }) } }, /** * @param {JSONNode | YAMLNode} node */ - leaveNode(node: N) { - if (pathStack.node === node) { + leaveKey(reportNode: N | null) { + if (pathStack.node === reportNode) { pathStack = pathStack.upper! } }, @@ -182,71 +151,49 @@ function create(context: RuleContext): RuleListener { * @param {UsedKeys} usedKeys */ function createVisitorForJson(sourceCode: SourceCode, usedKeys: UsedKeys) { - return createVisitor(usedKeys, { - /** - * @param {JSONNode} node - */ - skipNode(node) { - if ( - node.type === 'Program' || - node.type === 'JSONExpressionStatement' || - node.type === 'JSONProperty' - ) { - return true - } - const parent = node.parent! - if (parent.type === 'JSONProperty' && parent.key === node) { - return true - } - return false + const verifyContext = createVerifyContext(usedKeys, { + buildFixer(node: JSONAST.JSONNode) { + return fixer => fixer.removeRange(fixRemoveRange(node)) }, - /** - * @param {JSONNode} node - */ - resolveKey(node) { - const parent = node.parent! - if (parent.type === 'JSONProperty') { - return parent.key.type === 'JSONLiteral' - ? `${parent.key.value}` - : parent.key.name - } else if (parent.type === 'JSONArrayExpression') { - return parent.elements.indexOf(node as never) + buildAllFixer(nodes: JSONAST.JSONNode[]) { + return function* (fixer) { + yield* fixAllRemoveKeys(fixer, nodes) } + } + }) + function isIgnore(node: JSONAST.JSONExpression) { + return ( + node.type === 'JSONArrayExpression' || + node.type === 'JSONObjectExpression' + ) + } + return { + JSONProperty(node: JSONAST.JSONProperty) { + const key = + node.key.type === 'JSONLiteral' ? `${node.key.value}` : node.key.name - return null + verifyContext.enterKey(key, node.key, isIgnore(node.value)) }, - - /** - * @param {JSONNode} node - */ - resolveReportNode(node) { - if ( - node.type === 'JSONObjectExpression' || - node.type === 'JSONArrayExpression' - ) { - // ignore report - return null + 'JSONProperty:exit'(node: JSONAST.JSONProperty) { + verifyContext.leaveKey(node.key) + }, + 'JSONArrayExpression > *'( + node: JSONAST.JSONArrayExpression['elements'][number] & { + parent: JSONAST.JSONArrayExpression } - const parent = node.parent! - return parent.type === 'JSONProperty' ? parent.key : node + ) { + const key = node.parent.elements.indexOf(node) + verifyContext.enterKey(key, node, isIgnore(node)) }, - - /** - * @param {JSONNode} node - */ - buildFixer(node) { - return fixer => fixer.removeRange(fixRemoveRange(node)) + 'JSONArrayExpression > *:exit'( + node: JSONAST.JSONArrayExpression['elements'][number] + ) { + verifyContext.leaveKey(node!) }, - - /** - * @param {JSONNode[]} node - */ - buildAllFixer(nodes) { - return function* (fixer) { - yield* fixAllRemoveKeys(fixer, nodes) - } + 'Program:exit'() { + verifyContext.reports() } - }) + } function* fixAllRemoveKeys(fixer: RuleFixer, nodes: JSONAST.JSONNode[]) { const ranges = nodes.map(node => fixRemoveRange(node)) @@ -301,76 +248,10 @@ function create(context: RuleContext): RuleListener { /** * Create node visitor for YAML - * @param {SourceCode} sourceCode - * @param {UsedKeys} usedKeys */ function createVisitorForYaml(sourceCode: SourceCode, usedKeys: UsedKeys) { - const yamlKeyNodes = new Set() - return createVisitor(usedKeys, { - /** - * @param {YAMLNode} node - */ - skipNode(node) { - if ( - node.type === 'Program' || - node.type === 'YAMLDocument' || - node.type === 'YAMLDirective' || - node.type === 'YAMLAnchor' || - node.type === 'YAMLTag' - ) { - return true - } - - if (yamlKeyNodes.has(node)) { - // within key node - return true - } - const parent = node.parent - if (yamlKeyNodes.has(parent)) { - // within key node - yamlKeyNodes.add(node) - return true - } - if (node.type === 'YAMLPair') { - yamlKeyNodes.add(node.key) - return true - } - return false - }, - /** - * @param {YAMLContent | YAMLWithMeta} node - */ - resolveKey(node) { - const parent = node.parent! - if (parent.type === 'YAMLPair' && parent.key) { - const key = - parent.key.type !== 'YAMLScalar' - ? sourceCode.getText(parent.key) - : parent.key.value - return typeof key === 'boolean' || key === null ? String(key) : key - } else if (parent.type === 'YAMLSequence') { - return parent.entries.indexOf(node as never) - } - - return null - }, - - /** - * @param {YAMLContent | YAMLWithMeta} node - */ - resolveReportNode(node) { - if (node.type === 'YAMLMapping' || node.type === 'YAMLSequence') { - // ignore report - return null - } - const parent = node.parent! - return parent.type === 'YAMLPair' ? parent.key : node - }, - - /** - * @param {YAMLNode} node - */ - buildFixer(node) { + const verifyContext = createVerifyContext(usedKeys, { + buildFixer(node: YAMLAST.YAMLNode) { return function* (fixer) { const parentToCheck = node.parent! const removeNode = @@ -387,11 +268,7 @@ function create(context: RuleContext): RuleListener { } } }, - - /** - * @param {YAMLNode[]} node - */ - buildAllFixer(nodes) { + buildAllFixer(nodes: YAMLAST.YAMLNode[]) { return function* (fixer) { const removed = new Set() /** @type {YAMLNode[]} */ @@ -460,7 +337,63 @@ function create(context: RuleContext): RuleListener { } } }) + const yamlKeyNodes = new Set() + function withinKey(node: YAMLAST.YAMLNode) { + for (const keyNode of yamlKeyNodes) { + if ( + keyNode.range[0] <= node.range[0] && + node.range[0] < keyNode.range[1] + ) { + return true + } + } + return false + } + function isIgnore(node: YAMLAST.YAMLContent | YAMLAST.YAMLWithMeta | null) { + return Boolean( + node && (node.type === 'YAMLMapping' || node.type === 'YAMLSequence') + ) + } + return { + YAMLPair(node: YAMLAST.YAMLPair) { + if (node.key != null) { + if (withinKey(node)) { + return + } + yamlKeyNodes.add(node.key) + } else { + return + } + + const keyValue = + node.key.type !== 'YAMLScalar' + ? sourceCode.getText(node.key) + : node.key.value + const key = + typeof keyValue === 'boolean' || keyValue === null + ? String(keyValue) + : keyValue + verifyContext.enterKey(key, node.key, isIgnore(node.value)) + }, + 'YAMLPair:exit'(node: YAMLAST.YAMLPair) { + verifyContext.leaveKey(node.key) + }, + 'YAMLSequence > *'( + node: YAMLAST.YAMLSequence['entries'][number] & { + parent: YAMLAST.YAMLSequence + } + ) { + const key = node.parent.entries.indexOf(node) + verifyContext.enterKey(key, node, isIgnore(node)) + }, + 'YAMLSequence > *:exit'(node: YAMLAST.YAMLSequence['entries'][number]) { + verifyContext.leaveKey(node!) + }, + 'Program:exit'() { + verifyContext.reports() + } + } /** * @param {RuleFixer} fixer * @param {YAMLNode} removeNode @@ -548,75 +481,49 @@ function create(context: RuleContext): RuleListener { } if (extname(filename) === '.vue') { - return { - Program(node: VAST.ESLintProgram) { - const documentFragment = - context.parserServices.getDocumentFragment && - context.parserServices.getDocumentFragment() - /** @type {VElement[]} */ - const i18nBlocks = - (documentFragment && - documentFragment.children.filter( - (node): node is VAST.VElement => - node.type === 'VElement' && node.name === 'i18n' - )) || - [] - if (!i18nBlocks.length) { - return - } + return defineCustomBlocksVisitor( + context, + ctx => { const localeMessages = getLocaleMessages(context) const usedLocaleMessageKeys = collectKeysFromAST( - node, + context.getSourceCode().ast as VAST.ESLintProgram, context.getSourceCode().visitorKeys ) + const targetLocaleMessage = localeMessages.findBlockLocaleMessage( + ctx.parserServices.customBlock + ) + if (!targetLocaleMessage) { + return {} + } + const usedKeys = getUsedKeysMap( + targetLocaleMessage, + targetLocaleMessage.messages, + usedLocaleMessageKeys + ) - for (const block of i18nBlocks) { - if ( - block.startTag.attributes.some( - attr => !attr.directive && attr.key.name === 'src' - ) - ) { - continue - } - - const targetLocaleMessage = localeMessages.findBlockLocaleMessage( - block - ) - if (!targetLocaleMessage) { - continue - } - const sourceCode = targetLocaleMessage.getSourceCode() - if (!sourceCode) { - continue - } - const usedKeys = getUsedKeysMap( - targetLocaleMessage, - targetLocaleMessage.messages, - usedLocaleMessageKeys - ) - - const parserLang = targetLocaleMessage.getParserLang() - - let visitor - if (parserLang === 'json') { - visitor = createVisitorForJson(sourceCode, usedKeys) - } else if (parserLang === 'yaml') { - visitor = createVisitorForYaml(sourceCode, usedKeys) - } - - if (visitor == null) { - return - } - - targetLocaleMessage.traverseNodes({ - enterNode: visitor.enterNode, - leaveNode: visitor.leaveNode - }) - - visitor.reports() + return createVisitorForJson(ctx.getSourceCode(), usedKeys) + }, + ctx => { + const localeMessages = getLocaleMessages(context) + const usedLocaleMessageKeys = collectKeysFromAST( + context.getSourceCode().ast as VAST.ESLintProgram, + context.getSourceCode().visitorKeys + ) + const targetLocaleMessage = localeMessages.findBlockLocaleMessage( + ctx.parserServices.customBlock + ) + if (!targetLocaleMessage) { + return {} } + const usedKeys = getUsedKeysMap( + targetLocaleMessage, + targetLocaleMessage.messages, + usedLocaleMessageKeys + ) + + return createVisitorForYaml(ctx.getSourceCode(), usedKeys) } - } + ) } else if (context.parserServices.isJSON || context.parserServices.isYAML) { const localeMessages = getLocaleMessages(context) const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename) @@ -639,31 +546,9 @@ function create(context: RuleContext): RuleListener { usedLocaleMessageKeys ) if (context.parserServices.isJSON) { - const { enterNode, leaveNode, reports } = createVisitorForJson( - sourceCode, - usedKeys - ) - - return { - '[type=/^JSON/]': enterNode, - '[type=/^JSON/]:exit': leaveNode, - 'Program:exit'() { - reports() - } - } + return createVisitorForJson(sourceCode, usedKeys) } else if (context.parserServices.isYAML) { - const { enterNode, leaveNode, reports } = createVisitorForYaml( - sourceCode, - usedKeys - ) - - return { - '[type=/^YAML/]': enterNode, - '[type=/^YAML/]:exit': leaveNode, - 'Program:exit'() { - reports() - } - } + return createVisitorForYaml(sourceCode, usedKeys) } return {} } else { diff --git a/lib/types/vue-parser-services.ts b/lib/types/vue-parser-services.ts index df04bc99..97a85f28 100644 --- a/lib/types/vue-parser-services.ts +++ b/lib/types/vue-parser-services.ts @@ -1,6 +1,8 @@ import type { Rule } from 'eslint' +import type { RuleContext } from './eslint' import type { AST as VAST } from 'vue-eslint-parser' import type { TokenStore } from './types' +import { VElement } from 'vue-eslint-parser/ast' export interface TemplateListener { [key: string]: ((node: never) => void) | undefined @@ -33,5 +35,24 @@ export interface VueParserServices { templateBodyVisitor: TemplateListener, scriptVisitor?: RuleListener ) => RuleListener + defineCustomBlocksVisitor?: ( + context: RuleContext, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parser: { parseForESLint: (code: string, options: any) => any }, + rule: { + target: + | string + | string[] + | ((lang: string | null, customBlock: VAST.VElement) => boolean) + create: CustomBlockVisitorFactory + }, + scriptVisitor?: RuleListener + ) => RuleListener getDocumentFragment?: () => VAST.VDocumentFragment | null } + +export type CustomBlockVisitorFactory = ( + context: RuleContext & { + parserServices: RuleContext['parserServices'] & { customBlock: VElement } + } +) => RuleListener | null diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 5ba51b69..bad0c601 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -19,8 +19,11 @@ import type { LocaleKeyType, SettingsVueI18nLocaleDir, SettingsVueI18nLocaleDirObject, - SettingsVueI18nLocaleDirGlob + SettingsVueI18nLocaleDirGlob, + CustomBlockVisitorFactory } from '../types' +import * as jsoncESLintParser from 'jsonc-eslint-parser' +import * as yamlESLintParser from 'yaml-eslint-parser' interface LocaleFiles { files: string[] @@ -240,3 +243,64 @@ function getLocaleMessagesFromI18nBlocks( i18nBlockLocaleMessages.set(sourceCode.ast, localeMessages) return localeMessages } + +export function defineCustomBlocksVisitor( + context: RuleContext, + jsonRule: CustomBlockVisitorFactory, + yamlRule: CustomBlockVisitorFactory +): RuleListener { + if (!context.parserServices.defineCustomBlocksVisitor) { + return {} + } + const jsonVisitor = context.parserServices.defineCustomBlocksVisitor( + context, + jsoncESLintParser, + { + target(lang: string | null, block: VAST.VElement): boolean { + if (block.name !== 'i18n') { + return false + } + return !lang || lang === 'json' || lang === 'json5' + }, + create: jsonRule + } + ) + const yamlVisitor = context.parserServices.defineCustomBlocksVisitor( + context, + yamlESLintParser, + { + target(lang: string | null, block: VAST.VElement): boolean { + if (block.name !== 'i18n') { + return false + } + return lang === 'yaml' || lang === 'yml' + }, + create: yamlRule + } + ) + + return compositingVisitors(jsonVisitor, yamlVisitor) +} + +function compositingVisitors( + visitor: RuleListener, + ...visitors: RuleListener[] +) { + for (const v of visitors) { + for (const key in v) { + if (visitor[key]) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const o = visitor[key] as any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + visitor[key] = (...args: any[]) => { + o(...args) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(v[key] as any)(...args) + } + } else { + visitor[key] = v[key] + } + } + } + return visitor +} diff --git a/lib/utils/locale-messages.ts b/lib/utils/locale-messages.ts index 0fccb64a..b367e2c3 100644 --- a/lib/utils/locale-messages.ts +++ b/lib/utils/locale-messages.ts @@ -2,23 +2,18 @@ * @fileoverview Classes that acquires and manages localization messages * @author Yosuke Ota */ -import type { AST as JSONAST } from 'jsonc-eslint-parser' -import type { AST as YAMLAST } from 'yaml-eslint-parser' import type { AST as VAST } from 'vue-eslint-parser' import type { RuleContext, I18nLocaleMessageDictionary, I18nLocaleMessageValue, - SourceCode, LocaleKeyType } from '../types' import { extname } from 'path' import fs from 'fs' import { - parseJsonInI18nBlock, - parseYamlInI18nBlock, - Visitor, - Parsed + parseYamlValuesInI18nBlock, + parseJsonValuesInI18nBlock } from './parsers' import { ResourceLoader } from './resource-loader' import JSON5 from 'json5' @@ -84,39 +79,6 @@ export abstract class LocaleMessage { return (this._locales = []) } - /** - * Check if the message with the given key exists. - * @param {string} locale The locale name - * @param {string} key The given key to check - * @returns {boolean} - */ - hasMessage(locale: string, key: string): boolean { - return this.getMessage(locale, key) != null - } - - /** - * Gets the message for the given key. - * @param {string} locale The locale name - * @param {string} key The given key - * @returns {any} The message for the given key. `null` if the message is missing. - */ - getMessage(locale: string, key: string): I18nLocaleMessageValue | null { - const paths = key.split('.') - const length = paths.length - let last: I18nLocaleMessageValue = this.getMessagesFromLocale(locale) - let i = 0 - while (i < length) { - const value: I18nLocaleMessageValue | undefined = - last && typeof last !== 'string' ? last[paths[i]] : undefined - if (value == null) { - return null - } - last = value - i++ - } - return last ?? null - } - /** * Gets messages for the given locale. */ @@ -135,10 +97,8 @@ export abstract class LocaleMessage { } export class BlockLocaleMessage extends LocaleMessage { - private context: RuleContext public readonly block: VAST.VElement private lang: string - private _parsed: Parsed | null = null private _messages: I18nLocaleMessageDictionary | null = null /** * @param {object} arg @@ -155,7 +115,6 @@ export class BlockLocaleMessage extends LocaleMessage { fullpath, locales, localeKey, - context, lang = 'json' }: { block: VAST.VElement @@ -171,7 +130,6 @@ export class BlockLocaleMessage extends LocaleMessage { localeKey }) - this.context = context this.block = block this.lang = lang || 'json' } @@ -180,57 +138,15 @@ export class BlockLocaleMessage extends LocaleMessage { if (this._messages) { return this._messages } - const parsed = this._getParsed() - if (!parsed) { - return {} - } - return (this._messages = parsed.getStaticValue(parsed.ast)) - } - - getAST(): JSONAST.JSONProgram | YAMLAST.YAMLProgram | null { - const parsed = this._getParsed() - if (!parsed) { - return null - } - return parsed.ast - } - getParserLang(): string | null { - const parsed = this._getParsed() - if (!parsed) { - return null - } - return parsed.lang - } - /** - * @param {Visitor} visitor - */ - traverseNodes(visitor: Visitor): void { - const parsed = this._getParsed() - if (!parsed) { - return - } - parsed.traverseNodes(parsed.ast, visitor) - } - getSourceCode(): SourceCode | null { - const parsed = this._getParsed() - if (!parsed) { - return null - } - return parsed.getSourceCode() - } - - _getParsed(): Parsed | null { - if (this._parsed) { - return this._parsed - } - const { lang } = this if (lang === 'json' || lang === 'json5') { - return (this._parsed = parseJsonInI18nBlock(this.context, this.block)) + this._messages = parseJsonValuesInI18nBlock(this.block) || {} } else if (lang === 'yaml' || lang === 'yml') { - return (this._parsed = parseYamlInI18nBlock(this.context, this.block)) + this._messages = parseYamlValuesInI18nBlock(this.block) || {} + } else { + this._messages = {} } - return null + return this._messages } } diff --git a/lib/utils/parsers/index.ts b/lib/utils/parsers/index.ts index 0c0dfbce..a5fffe6f 100644 --- a/lib/utils/parsers/index.ts +++ b/lib/utils/parsers/index.ts @@ -2,59 +2,10 @@ * @fileoverview parser for block * @author Yosuke Ota */ -import type { AST as JSONAST } from 'jsonc-eslint-parser' -import type { AST as YAMLAST } from 'yaml-eslint-parser' -import { - parseForESLint as parseJsonForESLint, - getStaticJSONValue -} from 'jsonc-eslint-parser' -import { - parseForESLint as parseYamlForESLint, - getStaticYAMLValue -} from 'yaml-eslint-parser' -import { SourceCode } from 'eslint' -import { AST as VAST } from 'vue-eslint-parser' -import { LocationFixer } from './location-fixer' -import type { - RuleContext, - MaybeNode, - I18nLocaleMessageDictionary, - SourceCode as VSourceCode, - VisitorKeys -} from '../../types' - -export type Visitor = { - enterNode( - node: JSONAST.JSONNode | YAMLAST.YAMLNode, - parent: JSONAST.JSONNode | YAMLAST.YAMLNode - ): void - leaveNode( - node: JSONAST.JSONNode | YAMLAST.YAMLNode, - parent: JSONAST.JSONNode | YAMLAST.YAMLNode - ): void -} - -export interface Parsed { - lang: 'json' | 'yaml' - ast: N & { type: 'Program' } - getStaticValue(node?: N): I18nLocaleMessageDictionary - traverseNodes( - node: JSONAST.JSONNode | YAMLAST.YAMLNode, - visitor: Visitor - ): void - sourceString: string - getSourceCode(): VSourceCode -} - -export interface JSONParsed extends Parsed { - lang: 'json' - ast: JSONAST.JSONProgram -} - -export interface YAMLParsed extends Parsed { - lang: 'yaml' - ast: YAMLAST.YAMLProgram -} +import JSON5 from 'json5' +import yaml from 'js-yaml' +import type { AST as VAST } from 'vue-eslint-parser' +import type { I18nLocaleMessageDictionary } from '../../types' function hasEndTag( element: VAST.VElement @@ -62,176 +13,43 @@ function hasEndTag( return !!element.endTag } -function getSourceCodeString( - context: RuleContext, - i18nBlock: VAST.VElement & { endTag: VAST.VEndTag } -): string { - const tokenStore = context.parserServices.getTemplateBodyTokenStore() - const tokens = tokenStore.getTokensBetween( - i18nBlock.startTag, - i18nBlock.endTag - ) - if ( - tokens.length || - i18nBlock.startTag.range[1] === i18nBlock.endTag.range[0] - ) { - return tokens.map(t => t.value).join('') - } - // without - const df = context.parserServices.getDocumentFragment?.() - if (!df) { - return '' - } - const start = i18nBlock.startTag.range[1] - const end = i18nBlock.endTag.range[0] - let sourceCode = '' - for (const token of df.tokens) { - if (start <= token.range[0] && token.range[1] <= end) { - sourceCode += token.value - } - if (end <= token.range[0]) { - break - } - } - return sourceCode -} - /** * @param {RuleContext} context * @param {VElement} i18nBlock */ -function parseInI18nBlock

( - context: RuleContext, +function parseValuesInI18nBlock( i18nBlock: VAST.VElement, - parseForESLint: ( - code: string, - option: unknown - ) => { ast: P; visitorKeys: VisitorKeys } + parse: (code: string) => I18nLocaleMessageDictionary ) { if (!hasEndTag(i18nBlock)) { return null } - const sourceString = getSourceCodeString(context, i18nBlock) + const text = i18nBlock.children[0] + const sourceString = text != null && text.type === 'VText' ? text.value : '' if (!sourceString.trim()) { return null } - const offsetIndex = i18nBlock.startTag.range[1] - const sourceCode = context.getSourceCode() - const locationFixer = new LocationFixer( - sourceCode, - offsetIndex, - sourceCode.text.slice(offsetIndex, i18nBlock.endTag.range[0]), - sourceString - ) - - let ast: P, visitorKeys: VisitorKeys try { - const result = parseForESLint(sourceString, { - ecmaVersion: 2019, - loc: true, - range: true, - raw: true, - tokens: true, - comment: true, - eslintVisitorKeys: true, - eslintScopeManager: true - }) - ast = result.ast - visitorKeys = result.visitorKeys! + return parse(sourceString) } catch (e) { - const { line, column } = locationFixer.getFixLoc( - e.lineNumber, - e.column, - e.index - ) - context.report({ - message: e.message, - loc: { line, column } - }) return null } - - // fix locations - VAST.traverseNodes(ast as never, { - visitorKeys, - enterNode(node, parent) { - node.parent = parent || null - - locationFixer.fixLocations(node) - }, - leaveNode() { - // noop - } - }) - for (const token of ast.tokens || []) { - locationFixer.fixLocations(token) - } - for (const comment of ast.comments || []) { - locationFixer.fixLocations(comment as MaybeNode) - } - - let resourceSourceCode: VSourceCode - return { - ast, - sourceString, - getSourceCode(): VSourceCode { - return ( - resourceSourceCode || - (resourceSourceCode = new SourceCode( - sourceCode.text, - ast as never - ) as VSourceCode) - ) - }, - traverseNodes(node: JSONAST.JSONNode | YAMLAST.YAMLNode, visitor: Visitor) { - VAST.traverseNodes( - node as never, - { - visitorKeys, - ...visitor - } as never - ) - } - } } /** - * @param {RuleContext} context * @param {VElement} i18nBlock */ -export function parseJsonInI18nBlock( - context: RuleContext, +export function parseJsonValuesInI18nBlock( i18nBlock: VAST.VElement -): JSONParsed | null { - const result = parseInI18nBlock(context, i18nBlock, parseJsonForESLint) - if (result == null) { - return result - } - return { - lang: 'json', - getStaticValue(node: JSONAST.JSONNode): I18nLocaleMessageDictionary { - return getStaticJSONValue(node) as never - }, - ...result - } +): I18nLocaleMessageDictionary | null { + return parseValuesInI18nBlock(i18nBlock, code => JSON5.parse(code)) } + /** - * @param {RuleContext} context * @param {VElement} i18nBlock */ -export function parseYamlInI18nBlock( - context: RuleContext, +export function parseYamlValuesInI18nBlock( i18nBlock: VAST.VElement -): YAMLParsed | null { - const result = parseInI18nBlock(context, i18nBlock, parseYamlForESLint) - if (result == null) { - return result - } - return { - lang: 'yaml', - getStaticValue(node: YAMLAST.YAMLNode): I18nLocaleMessageDictionary { - return getStaticYAMLValue(node as never) as never - }, - ...result - } +): I18nLocaleMessageDictionary | null { + return parseValuesInI18nBlock(i18nBlock, code => yaml.safeLoad(code) as never) } diff --git a/lib/utils/parsers/location-fixer.ts b/lib/utils/parsers/location-fixer.ts deleted file mode 100644 index 0adabf1f..00000000 --- a/lib/utils/parsers/location-fixer.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * @fileoverview AST location fixer - * @author Yosuke Ota - */ -import diff from 'fast-diff' -import type { SourceCode, MaybeNode, Position, Range } from '../../types' - -export class LocationFixer { - private sourceCode: SourceCode - constructor( - sourceCode: SourceCode, - offsetIndex: number, - original: string, - cooked: string - ) { - this.sourceCode = sourceCode - if (original === cooked) { - const offsetLoc = sourceCode.getLocFromIndex(offsetIndex) - this.getFixLoc = (line, column) => { - if (line === 1) { - return { - line: line + offsetLoc.line - 1, - column: column + offsetLoc.column - } - } else { - return { line: line + offsetLoc.line - 1, column } - } - } - } else { - const indexMap = new IndexMap(original, cooked) - this.getFixLoc = (_line, _column, index) => { - const origIndex = indexMap.remapIndex(index) + offsetIndex - return sourceCode.getLocFromIndex(origIndex) - } - } - } - getFixLoc( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _line: number, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _column: number, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _index: number - ): // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - Position { - // Defined by the constructor. - } - fixLocations(node: MaybeNode): void { - const sourceCode = this.sourceCode - const startLoc = this.getFixLoc( - node.loc.start.line, - node.loc.start.column, - node.range[0] - ) - node.loc.start = startLoc - node.range[0] = sourceCode.getIndexFromLoc(startLoc) - - const endLoc = this.getFixLoc( - node.loc.end.line, - node.loc.end.column, - node.range[1] - ) - node.loc.end = endLoc - node.range[1] = sourceCode.getIndexFromLoc(endLoc) - } -} - -/** - * A class to remap the index which is shifted by HTML escape. - */ -class IndexMap { - private mappers: { - org: Range - new: Range - }[] - private orgIndex: number - private newIndex: number - private batchLengthOrg: number - private batchLengthNew: number - constructor(original: string, cooked: string) { - this.mappers = [] - this.orgIndex = 0 - this.newIndex = 0 - this.batchLengthOrg = 0 - this.batchLengthNew = 0 - - const results = diff(original, cooked) - for (const [op, text] of results) { - switch (op) { - case diff.INSERT: - this.applyIns(text) - break - case diff.DELETE: - this.applyDel(text) - break - case diff.EQUAL: - this.applyEq(text) - break - default: - throw new Error(`Unexpected fast-diff operation "${op}"`) - } - } - this.flush() - } - - applyEq(text: string) { - this.flush() - const newEnd = this.newIndex + text.length - const orgEnd = this.orgIndex + text.length - this.addMap([this.orgIndex, orgEnd], [this.newIndex, newEnd]) - this.newIndex = newEnd - this.orgIndex = orgEnd - } - - applyIns(text: string) { - this.batchLengthNew += text.length - } - - applyDel(text: string) { - this.batchLengthOrg += text.length - } - - flush() { - if (this.batchLengthNew || this.batchLengthOrg) { - const newEnd = this.newIndex + this.batchLengthNew - const orgEnd = this.orgIndex + this.batchLengthOrg - this.addMap([this.orgIndex, orgEnd], [this.newIndex, newEnd]) - this.newIndex = newEnd - this.orgIndex = orgEnd - this.batchLengthOrg = 0 - this.batchLengthNew = 0 - } - } - - addMap(orgRange: Range, newRange: Range) { - if (orgRange[0] === newRange[0] && orgRange[1] === newRange[1]) { - return - } - this.mappers.unshift({ - org: orgRange, - new: newRange - }) - } - - remapIndex(index: number) { - for (const mapper of this.mappers) { - if (mapper.new[0] <= index && index < mapper.new[1]) { - const offset = index - mapper.new[0] - return Math.min(mapper.org[0] + offset, mapper.org[1] - 1) - } - } - return index - } -} diff --git a/package.json b/package.json index 1cc27a40..e951c34b 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ } }, "dependencies": { - "fast-diff": "^1.2.0", "glob": "^7.1.3", "ignore": "^5.0.5", "js-yaml": "^3.14.0", @@ -32,20 +31,21 @@ "jsonc-eslint-parser": "^0.6.0", "lodash": "^4.17.11", "parse5": "^6.0.0", - "vue-eslint-parser": "^7.0.0", + "vue-eslint-parser": "^7.3.0", "yaml-eslint-parser": "^0.2.0" }, "devDependencies": { "@types/debug": "^4.1.5", "@types/eslint": "^7.2.0", "@types/eslint-scope": "^3.7.0", + "@types/eslint-visitor-keys": "^1.0.0", "@types/js-yaml": "^3.12.5", "@types/json5": "^0.0.30", "@types/lodash": "^4.14.159", "@types/mocha": "^8.0.1", "@types/parse5": "^5.0.3", - "@typescript-eslint/eslint-plugin": "^3.8.0", - "@typescript-eslint/parser": "^3.8.0", + "@typescript-eslint/eslint-plugin": "^4.10.0", + "@typescript-eslint/parser": "^4.10.0", "eslint": "^5.15.0 || ^6.0.0 || ^7.0.0", "eslint-config-prettier": "^7.0.0", "eslint-plugin-markdown": "^1.0.0", @@ -103,7 +103,7 @@ "release:trigger": "shipjs trigger", "test": "mocha --require ts-node/register \"./tests/**/*.ts\"", "test:debug": "mocha --require ts-node/register --inspect \"./tests/**/*.ts\"", - "test:coverage": "nyc mocha --require ts-node/register \"./tests/**/*.ts\"", + "test:coverage": "nyc mocha --require ts-node/register \"./tests/**/*.ts\" --timeout 60000", "test:integrations": "mocha ./tests-integrations/*.js --timeout 60000" } } diff --git a/tests/lib/rules/no-html-messages.ts b/tests/lib/rules/no-html-messages.ts index 2b640f9f..bf9c5574 100644 --- a/tests/lib/rules/no-html-messages.ts +++ b/tests/lib/rules/no-html-messages.ts @@ -15,12 +15,10 @@ new RuleTester({ { // sfc supports filename: 'test.vue', - code: `${fs - .readFileSync( - require.resolve('../../fixtures/no-html-messages/valid/en.json'), - 'utf8' - ) - .replace(/ + code: `${fs.readFileSync( + require.resolve('../../fixtures/no-html-messages/valid/en.json'), + 'utf8' + )} ` }, { @@ -42,12 +40,10 @@ new RuleTester({ { // sfc supports filename: 'test.vue', - code: `${fs - .readFileSync( - require.resolve('../../fixtures/no-html-messages/invalid/en.json'), - 'utf8' - ) - .replace(/ + code: `${fs.readFileSync( + require.resolve('../../fixtures/no-html-messages/invalid/en.json'), + 'utf8' + )} `, errors: [ { @@ -70,12 +66,10 @@ new RuleTester({ { // sfc supports filename: 'test.vue', - code: `${fs - .readFileSync( - require.resolve('../../fixtures/no-html-messages/invalid/en.yaml'), - 'utf8' - ) - .replace(/ + code: `${fs.readFileSync( + require.resolve('../../fixtures/no-html-messages/invalid/en.yaml'), + 'utf8' + )} `, errors: [ { diff --git a/yarn.lock b/yarn.lock index 9e996c7c..fb048f91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1239,65 +1239,75 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== -"@typescript-eslint/eslint-plugin@^3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.8.0.tgz#f82947bcdd9a4e42be7ad80dfd61f1dc411dd1df" - integrity sha512-lFb4VCDleFSR+eo4Ew+HvrJ37ZH1Y9ZyE+qyP7EiwBpcCVxwmUc5PAqhShCQ8N8U5vqYydm74nss+a0wrrCErw== +"@typescript-eslint/eslint-plugin@^4.10.0": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.10.0.tgz#19ed3baf4bc4232c5a7fcd32eaca75c3a5baf9f3" + integrity sha512-h6/V46o6aXpKRlarP1AiJEXuCJ7cMQdlpfMDrcllIgX3dFkLwEBTXAoNP98ZoOmqd1xvymMVRAI4e7yVvlzWEg== dependencies: - "@typescript-eslint/experimental-utils" "3.8.0" + "@typescript-eslint/experimental-utils" "4.10.0" + "@typescript-eslint/scope-manager" "4.10.0" debug "^4.1.1" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.8.0.tgz#ac1f7c88322dcfb7635ece6f0441516dd951099a" - integrity sha512-o8T1blo1lAJE0QDsW7nSyvZHbiDzQDjINJKyB44Z3sSL39qBy5L10ScI/XwDtaiunoyKGLiY9bzRk4YjsUZl8w== +"@typescript-eslint/experimental-utils@4.10.0": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.10.0.tgz#dbf5d0f89802d5feaf7d11e5b32df29bbc2f3a0e" + integrity sha512-opX+7ai1sdWBOIoBgpVJrH5e89ra1KoLrJTz0UtWAa4IekkKmqDosk5r6xqRaNJfCXEfteW4HXQAwMdx+jjEmw== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/types" "3.8.0" - "@typescript-eslint/typescript-estree" "3.8.0" + "@typescript-eslint/scope-manager" "4.10.0" + "@typescript-eslint/types" "4.10.0" + "@typescript-eslint/typescript-estree" "4.10.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.8.0.tgz#8e1dcd404299bf79492409c81c415fa95a7c622b" - integrity sha512-u5vjOBaCsnMVQOvkKCXAmmOhyyMmFFf5dbkM3TIbg3MZ2pyv5peE4gj81UAbTHwTOXEwf7eCQTUMKrDl/+qGnA== +"@typescript-eslint/parser@^4.10.0": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.10.0.tgz#1a622b0847b765b2d8f0ede6f0cdd85f03d76031" + integrity sha512-amBvUUGBMadzCW6c/qaZmfr3t9PyevcSWw7hY2FuevdZVp5QPw/K76VSQ5Sw3BxlgYCHZcK6DjIhSZK0PQNsQg== dependencies: - "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "3.8.0" - "@typescript-eslint/types" "3.8.0" - "@typescript-eslint/typescript-estree" "3.8.0" - eslint-visitor-keys "^1.1.0" + "@typescript-eslint/scope-manager" "4.10.0" + "@typescript-eslint/types" "4.10.0" + "@typescript-eslint/typescript-estree" "4.10.0" + debug "^4.1.1" + +"@typescript-eslint/scope-manager@4.10.0": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.10.0.tgz#dbd7e1fc63d7363e3aaff742a6f2b8afdbac9d27" + integrity sha512-WAPVw35P+fcnOa8DEic0tQUhoJJsgt+g6DEcz257G7vHFMwmag58EfowdVbiNcdfcV27EFR0tUBVXkDoIvfisQ== + dependencies: + "@typescript-eslint/types" "4.10.0" + "@typescript-eslint/visitor-keys" "4.10.0" -"@typescript-eslint/types@3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.8.0.tgz#58581dd863f86e0cd23353d94362bb90b4bea796" - integrity sha512-8kROmEQkv6ss9kdQ44vCN1dTrgu4Qxrd2kXr10kz2NP5T8/7JnEfYNxCpPkArbLIhhkGLZV3aVMplH1RXQRF7Q== +"@typescript-eslint/types@4.10.0": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.10.0.tgz#12f983750ebad867f0c806e705c1953cd6415789" + integrity sha512-+dt5w1+Lqyd7wIPMa4XhJxUuE8+YF+vxQ6zxHyhLGHJjHiunPf0wSV8LtQwkpmAsRi1lEOoOIR30FG5S2HS33g== -"@typescript-eslint/typescript-estree@3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.8.0.tgz#0606d19f629f813dbdd5a34c7a1e895d6191cac6" - integrity sha512-MTv9nPDhlKfclwnplRNDL44mP2SY96YmPGxmMbMy6x12I+pERcxpIUht7DXZaj4mOKKtet53wYYXU0ABaiXrLw== +"@typescript-eslint/typescript-estree@4.10.0": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.10.0.tgz#1e62e45fd57866afd42daf5e9fb6bd4e8dbcfa75" + integrity sha512-mGK0YRp9TOk6ZqZ98F++bW6X5kMTzCRROJkGXH62d2azhghmq+1LNLylkGe6uGUOQzD452NOAEth5VAF6PDo5g== dependencies: - "@typescript-eslint/types" "3.8.0" - "@typescript-eslint/visitor-keys" "3.8.0" + "@typescript-eslint/types" "4.10.0" + "@typescript-eslint/visitor-keys" "4.10.0" debug "^4.1.1" - glob "^7.1.6" + globby "^11.0.1" is-glob "^4.0.1" lodash "^4.17.15" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.8.0.tgz#ad35110249fb3fc30a36bfcbfeea93e710cfaab1" - integrity sha512-gfqQWyVPpT9NpLREXNR820AYwgz+Kr1GuF3nf1wxpHD6hdxI62tq03ToomFnDxY0m3pUB39IF7sil7D5TQexLA== +"@typescript-eslint/visitor-keys@4.10.0": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.10.0.tgz#9478822329a9bc8ebcc80623d7f79a01da5ee451" + integrity sha512-hPyz5qmDMuZWFtHZkjcCpkAKHX8vdu1G3YsCLEd25ryZgnJfj6FQuJ5/O7R+dB1ueszilJmAFMtlU4CA6se3Jg== dependencies: - eslint-visitor-keys "^1.1.0" + "@typescript-eslint/types" "4.10.0" + eslint-visitor-keys "^2.0.0" "@vue/babel-helper-vue-jsx-merge-props@^1.0.0": version "1.0.0" @@ -1705,7 +1715,7 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -acorn-jsx@^5.0.0, acorn-jsx@^5.1.0: +acorn-jsx@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384" integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== @@ -1720,10 +1730,10 @@ acorn@^6.0.2, acorn@^6.2.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== -acorn@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" - integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== +acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^7.2.0: version "7.3.1" @@ -4210,6 +4220,11 @@ eslint-visitor-keys@^1.2.0, eslint-visitor-keys@^1.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== +eslint-visitor-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" + integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + "eslint@^5.15.0 || ^6.0.0 || ^7.0.0": version "7.4.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.4.0.tgz#4e35a2697e6c1972f9d6ef2b690ad319f80f206f" @@ -4275,13 +4290,13 @@ espree@^4.1.0: acorn-jsx "^5.2.0" eslint-visitor-keys "^1.3.0" -espree@^6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d" - integrity sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA== +espree@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" + integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== dependencies: - acorn "^7.1.0" - acorn-jsx "^5.1.0" + acorn "^7.1.1" + acorn-jsx "^5.2.0" eslint-visitor-keys "^1.1.0" espree@^7.1.0: @@ -4489,7 +4504,7 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== -fast-diff@^1.1.2, fast-diff@^1.2.0: +fast-diff@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== @@ -4517,6 +4532,18 @@ fast-glob@^3.0.3: merge2 "^1.3.0" micromatch "^4.0.2" +fast-glob@^3.1.1: + version "3.2.4" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" + integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -4984,6 +5011,18 @@ globby@^10.0.1: merge2 "^1.2.3" slash "^3.0.0" +globby@^11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" + integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -5450,6 +5489,11 @@ ignore@^5.0.5, ignore@^5.1.1: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + immediate@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.2.3.tgz#d140fa8f614659bd6541233097ddaac25cdd991c" @@ -7876,7 +7920,7 @@ picomatch@^2.0.4, picomatch@^2.0.5: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== -picomatch@^2.0.7: +picomatch@^2.0.7, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== @@ -10586,15 +10630,15 @@ vue-eslint-parser@^5.0.0: esquery "^1.0.1" lodash "^4.17.11" -vue-eslint-parser@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.0.0.tgz#a4ed2669f87179dedd06afdd8736acbb3a3864d6" - integrity sha512-yR0dLxsTT7JfD2YQo9BhnQ6bUTLsZouuzt9SKRP7XNaZJV459gvlsJo4vT2nhZ/2dH9j3c53bIx9dnqU2prM9g== +vue-eslint-parser@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.3.0.tgz#894085839d99d81296fa081d19643733f23d7559" + integrity sha512-n5PJKZbyspD0+8LnaZgpEvNCrjQx1DyDHw8JdWwoxhhC+yRip4TAvSDpXGf9SWX6b0umeB5aR61gwUo6NVvFxw== dependencies: debug "^4.1.1" eslint-scope "^5.0.0" eslint-visitor-keys "^1.1.0" - espree "^6.1.2" + espree "^6.2.1" esquery "^1.0.1" lodash "^4.17.15"