diff --git a/.vscode/settings.json b/.vscode/settings.json index 7bb646536..80e1f88fd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,7 +10,7 @@ "json", "jsonc" ], - "typescript.tsdk": "node_modules/typescript/lib", + "typescript.tsdk": "./node_modules/typescript/lib", "vetur.validation.script": false, "[typescript]": { "editor.formatOnSave": true, diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 5bcb7913b..68aec1af9 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -184,7 +184,8 @@ module.exports = { }, algolia: { - apiKey: 'b2b69365da747a9a9635cda391317c36', + appId: '2L4MGZSULB', + apiKey: 'fdf57932b27a6c230d01a890492ab76d', indexName: 'eslint-plugin-vue' } } diff --git a/docs/rules/README.md b/docs/rules/README.md index 5794cc3ed..735dc87d0 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -311,6 +311,7 @@ For example: | [vue/block-tag-newline](./block-tag-newline.md) | enforce line breaks after opening and before closing block-level tags | :wrench: | | [vue/component-api-style](./component-api-style.md) | enforce component API style | | | [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: | +| [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench::bulb: | | [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | | | [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | | | [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: | @@ -350,6 +351,7 @@ For example: | [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: | | [vue/no-v-text](./no-v-text.md) | disallow use of v-text | | | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | +| [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | | [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | | [vue/require-expose](./require-expose.md) | require declare public properties using `expose` | :bulb: | diff --git a/docs/rules/component-definition-name-casing.md b/docs/rules/component-definition-name-casing.md index 5e89ace75..615b876f9 100644 --- a/docs/rules/component-definition-name-casing.md +++ b/docs/rules/component-definition-name-casing.md @@ -31,7 +31,7 @@ Default casing is set to `PascalCase`. - `"PascalCase"` (default) ... enforce component definition names to pascal case. - `"kebab-case"` ... enforce component definition names to kebab case. -### `"PascalCase" (default) +### `"PascalCase"` (default) @@ -64,18 +64,18 @@ export default { ```js /* ✓ GOOD */ Vue.component('MyComponent', { - + }) /* ✗ BAD */ Vue.component('my-component', { - + }) ``` -### `"kebab-case" +### `"kebab-case"` @@ -108,12 +108,12 @@ export default { ```js /* ✓ GOOD */ Vue.component('my-component', { - + }) /* ✗ BAD */ Vue.component('MyComponent', { - + }) ``` diff --git a/docs/rules/component-options-name-casing.md b/docs/rules/component-options-name-casing.md new file mode 100644 index 000000000..d37ee9fce --- /dev/null +++ b/docs/rules/component-options-name-casing.md @@ -0,0 +1,169 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/component-options-name-casing +description: enforce the casing of component name in `components` options +since: v8.2.0 +--- +# vue/component-options-name-casing + +> enforce the casing of component name in `components` options + +- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule. +- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). + +## :book: Rule Details + +This rule aims to enforce casing of the component names in `components` options. + +## :wrench: Options + +```json +{ + "vue/component-options-name-casing": ["error", "PascalCase" | "kebab-case" | "camelCase"] +} +``` + +This rule has an option which can be one of these values: + +- `"PascalCase"` (default) ... enforce component names to pascal case. +- `"kebab-case"` ... enforce component names to kebab case. +- `"camelCase"` ... enforce component names to camel case. + +Please note that if you use kebab case in `components` options, +you can **only** use kebab case in template; +and if you use camel case in `components` options, +you **can't** use pascal case in template. + +For demonstration, the code example is invalid: + +```vue + + + +``` + +### `"PascalCase"` (default) + + + +```vue + +``` + + + + + +```vue + +``` + + + +### `"kebab-case"` + + + +```vue + +``` + + + + + +```vue + +``` + + + +### `"camelCase"` + + + +```vue + +``` + + + + + +```vue + +``` + + + +## :rocket: Version + +This rule was introduced in eslint-plugin-vue v8.2.0 + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/component-options-name-casing.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/component-options-name-casing.js) diff --git a/docs/rules/prefer-separate-static-class.md b/docs/rules/prefer-separate-static-class.md new file mode 100644 index 000000000..22df25e19 --- /dev/null +++ b/docs/rules/prefer-separate-static-class.md @@ -0,0 +1,47 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/prefer-separate-static-class +description: require static class names in template to be in a separate `class` attribute +since: v8.2.0 +--- +# vue/prefer-separate-static-class + +> require static class names in template to be in a separate `class` attribute + +- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule. + +## :book: Rule Details + +This rule reports static class names in dynamic class attributes. + + + +```vue + +``` + + + +## :wrench: Options + +Nothing. + +## :rocket: Version + +This rule was introduced in eslint-plugin-vue v8.2.0 + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-separate-static-class.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-separate-static-class.js) diff --git a/lib/index.js b/lib/index.js index c9969917c..3c628c559 100644 --- a/lib/index.js +++ b/lib/index.js @@ -24,6 +24,7 @@ module.exports = { 'component-api-style': require('./rules/component-api-style'), 'component-definition-name-casing': require('./rules/component-definition-name-casing'), 'component-name-in-template-casing': require('./rules/component-name-in-template-casing'), + 'component-options-name-casing': require('./rules/component-options-name-casing'), 'component-tags-order': require('./rules/component-tags-order'), 'custom-event-name-casing': require('./rules/custom-event-name-casing'), 'dot-location': require('./rules/dot-location'), @@ -153,6 +154,7 @@ module.exports = { 'operator-linebreak': require('./rules/operator-linebreak'), 'order-in-components': require('./rules/order-in-components'), 'padding-line-between-blocks': require('./rules/padding-line-between-blocks'), + 'prefer-separate-static-class': require('./rules/prefer-separate-static-class'), 'prefer-template': require('./rules/prefer-template'), 'prop-name-casing': require('./rules/prop-name-casing'), 'require-component-is': require('./rules/require-component-is'), diff --git a/lib/rules/component-options-name-casing.js b/lib/rules/component-options-name-casing.js new file mode 100644 index 000000000..ad70c1546 --- /dev/null +++ b/lib/rules/component-options-name-casing.js @@ -0,0 +1,115 @@ +/** + * @author Pig Fang + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') +const casing = require('../utils/casing') + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * @param {import('../../typings/eslint-plugin-vue/util-types/ast').Expression} node + * @returns {string | null} + */ +function getOptionsComponentName(node) { + if (node.type === 'Identifier') { + return node.name + } + if (node.type === 'Literal') { + return typeof node.value === 'string' ? node.value : null + } + return null +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: + 'enforce the casing of component name in `components` options', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/component-options-name-casing.html' + }, + fixable: 'code', + hasSuggestions: true, + schema: [{ enum: casing.allowedCaseOptions }], + messages: { + caseNotMatched: 'Component name "{{component}}" is not {{caseType}}.', + possibleRenaming: 'Rename component name to be in {{caseType}}.' + } + }, + /** @param {RuleContext} context */ + create(context) { + const caseType = context.options[0] || 'PascalCase' + + const canAutoFix = caseType === 'PascalCase' + const checkCase = casing.getChecker(caseType) + const convert = casing.getConverter(caseType) + + return utils.executeOnVue(context, (obj) => { + const node = utils.findProperty(obj, 'components') + if (!node || node.value.type !== 'ObjectExpression') { + return + } + + node.value.properties.forEach((property) => { + if (property.type !== 'Property') { + return + } + + const name = getOptionsComponentName(property.key) + if (!name || checkCase(name)) { + return + } + + context.report({ + node: property.key, + messageId: 'caseNotMatched', + data: { + component: name, + caseType + }, + fix: canAutoFix + ? (fixer) => { + const converted = convert(name) + return property.shorthand + ? fixer.replaceText(property, `${converted}: ${name}`) + : fixer.replaceText(property.key, converted) + } + : undefined, + suggest: canAutoFix + ? undefined + : [ + { + messageId: 'possibleRenaming', + data: { caseType }, + fix: (fixer) => { + const converted = convert(name) + if (caseType === 'kebab-case') { + return property.shorthand + ? fixer.replaceText(property, `'${converted}': ${name}`) + : fixer.replaceText(property.key, `'${converted}'`) + } + return property.shorthand + ? fixer.replaceText(property, `${converted}: ${name}`) + : fixer.replaceText(property.key, converted) + } + } + ] + }) + }) + }) + } +} diff --git a/lib/rules/no-dupe-keys.js b/lib/rules/no-dupe-keys.js index 61ffda40e..8b5e8e54c 100644 --- a/lib/rules/no-dupe-keys.js +++ b/lib/rules/no-dupe-keys.js @@ -14,14 +14,7 @@ const utils = require('../utils') // Rule Definition // ------------------------------------------------------------------------------ /** @type {GroupName[]} */ -const GROUP_NAMES = [ - 'props', - 'computed', - 'data', - 'asyncData', - 'methods', - 'setup' -] +const GROUP_NAMES = ['props', 'computed', 'data', 'methods', 'setup'] module.exports = { meta: { diff --git a/lib/rules/prefer-separate-static-class.js b/lib/rules/prefer-separate-static-class.js new file mode 100644 index 000000000..504d95d60 --- /dev/null +++ b/lib/rules/prefer-separate-static-class.js @@ -0,0 +1,231 @@ +/** + * @author Flo Edelmann + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const { defineTemplateBodyVisitor, getStringLiteralValue } = require('../utils') + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * @param {ASTNode} node + * @returns {node is Literal | TemplateLiteral} + */ +function isStringLiteral(node) { + return ( + (node.type === 'Literal' && typeof node.value === 'string') || + (node.type === 'TemplateLiteral' && node.expressions.length === 0) + ) +} + +/** + * @param {Expression | VForExpression | VOnExpression | VSlotScopeExpression | VFilterSequenceExpression} expressionNode + * @returns {(Literal | TemplateLiteral | Identifier)[]} + */ +function findStaticClasses(expressionNode) { + if (isStringLiteral(expressionNode)) { + return [expressionNode] + } + + if (expressionNode.type === 'ArrayExpression') { + return expressionNode.elements.flatMap((element) => { + if (element === null || element.type === 'SpreadElement') { + return [] + } + return findStaticClasses(element) + }) + } + + if (expressionNode.type === 'ObjectExpression') { + return expressionNode.properties.flatMap((property) => { + if ( + property.type === 'Property' && + property.value.type === 'Literal' && + property.value.value === true && + (isStringLiteral(property.key) || + (property.key.type === 'Identifier' && !property.computed)) + ) { + return [property.key] + } + return [] + }) + } + + return [] +} + +/** + * @param {VAttribute | VDirective} attributeNode + * @returns {attributeNode is VAttribute & { value: VLiteral }} + */ +function isStaticClassAttribute(attributeNode) { + return ( + !attributeNode.directive && + attributeNode.key.name === 'class' && + attributeNode.value !== null + ) +} + +/** + * Removes the node together with the comma before or after the node. + * @param {RuleFixer} fixer + * @param {ParserServices.TokenStore} tokenStore + * @param {ASTNode} node + */ +function* removeNodeWithComma(fixer, tokenStore, node) { + const prevToken = tokenStore.getTokenBefore(node) + if (prevToken.type === 'Punctuator' && prevToken.value === ',') { + yield fixer.removeRange([prevToken.range[0], node.range[1]]) + return + } + + const [nextToken, nextNextToken] = tokenStore.getTokensAfter(node, { + count: 2 + }) + if ( + nextToken.type === 'Punctuator' && + nextToken.value === ',' && + (nextNextToken.type !== 'Punctuator' || + (nextNextToken.value !== ']' && nextNextToken.value !== '}')) + ) { + yield fixer.removeRange([node.range[0], nextNextToken.range[0]]) + return + } + + yield fixer.remove(node) +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: + 'require static class names in template to be in a separate `class` attribute', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/prefer-separate-static-class.html' + }, + fixable: 'code', + schema: [], + messages: { + preferSeparateStaticClass: + 'Static class "{{className}}" should be in a static `class` attribute.' + } + }, + /** @param {RuleContext} context */ + create(context) { + return defineTemplateBodyVisitor(context, { + /** @param {VDirectiveKey} directiveKeyNode */ + "VAttribute[directive=true] > VDirectiveKey[name.name='bind'][argument.name='class']"( + directiveKeyNode + ) { + const attributeNode = directiveKeyNode.parent + if (!attributeNode.value || !attributeNode.value.expression) { + return + } + + const expressionNode = attributeNode.value.expression + const staticClassNameNodes = findStaticClasses(expressionNode) + + for (const staticClassNameNode of staticClassNameNodes) { + const className = + staticClassNameNode.type === 'Identifier' + ? staticClassNameNode.name + : getStringLiteralValue(staticClassNameNode, true) + + if (className === null) { + continue + } + + context.report({ + node: staticClassNameNode, + messageId: 'preferSeparateStaticClass', + data: { className }, + *fix(fixer) { + let dynamicClassDirectiveRemoved = false + + yield* removeFromClassDirective() + yield* addToClassAttribute() + + /** + * Remove class from dynamic `:class` directive. + */ + function* removeFromClassDirective() { + if (isStringLiteral(expressionNode)) { + yield fixer.remove(attributeNode) + dynamicClassDirectiveRemoved = true + return + } + + const listElement = + staticClassNameNode.parent.type === 'Property' + ? staticClassNameNode.parent + : staticClassNameNode + + const listNode = listElement.parent + if ( + listNode.type === 'ArrayExpression' || + listNode.type === 'ObjectExpression' + ) { + const elements = + listNode.type === 'ObjectExpression' + ? listNode.properties + : listNode.elements + + if (elements.length === 1 && listNode === expressionNode) { + yield fixer.remove(attributeNode) + dynamicClassDirectiveRemoved = true + return + } + + const tokenStore = + context.parserServices.getTemplateBodyTokenStore() + + if (elements.length === 1) { + yield* removeNodeWithComma(fixer, tokenStore, listNode) + return + } + + yield* removeNodeWithComma(fixer, tokenStore, listElement) + } + } + + /** + * Add class to static `class` attribute. + */ + function* addToClassAttribute() { + const existingStaticClassAttribute = + attributeNode.parent.attributes.find(isStaticClassAttribute) + if (existingStaticClassAttribute) { + const literalNode = existingStaticClassAttribute.value + yield fixer.replaceText( + literalNode, + `"${literalNode.value} ${className}"` + ) + return + } + + // new static `class` attribute + const separator = dynamicClassDirectiveRemoved ? '' : ' ' + yield fixer.insertTextBefore( + attributeNode, + `class="${className}"${separator}` + ) + } + } + }) + } + } + }) + } +} diff --git a/lib/rules/valid-define-emits.js b/lib/rules/valid-define-emits.js index 6f433a835..d7480d538 100644 --- a/lib/rules/valid-define-emits.js +++ b/lib/rules/valid-define-emits.js @@ -79,6 +79,7 @@ module.exports = { variable.defs.length && variable.defs.every( (def) => + def.type !== 'ImportBinding' && utils.inRange(scriptSetup.range, def.name) && !utils.inRange(defineEmits.range, def.name) ) diff --git a/lib/rules/valid-define-props.js b/lib/rules/valid-define-props.js index 3a084dd68..849437b1e 100644 --- a/lib/rules/valid-define-props.js +++ b/lib/rules/valid-define-props.js @@ -80,6 +80,7 @@ module.exports = { variable.defs.length && variable.defs.every( (def) => + def.type !== 'ImportBinding' && utils.inRange(scriptSetup.range, def.name) && !utils.inRange(defineProps.range, def.name) ) diff --git a/lib/utils/indent-common.js b/lib/utils/indent-common.js index 39ef58670..d208d5a08 100644 --- a/lib/utils/indent-common.js +++ b/lib/utils/indent-common.js @@ -19,7 +19,8 @@ const { isNotOpeningBraceToken, isOpeningBracketToken, isClosingBracketToken, - isSemicolonToken + isSemicolonToken, + isNotSemicolonToken } = require('eslint-utils') const { isComment, @@ -1288,13 +1289,13 @@ module.exports.defineVisitor = function create( }, /** @param {ExportAllDeclaration} node */ ExportAllDeclaration(node) { - const tokens = tokenStore.getTokens(node) - const firstToken = /** @type {Token} */ (tokens.shift()) - if (isSemicolonToken(tokens[tokens.length - 1])) { - tokens.pop() - } + const exportToken = tokenStore.getFirstToken(node) + const tokens = [ + ...tokenStore.getTokensBetween(exportToken, node.source), + tokenStore.getFirstToken(node.source) + ] if (!node.exported) { - setOffset(tokens, 1, firstToken) + setOffset(tokens, 1, exportToken) } else { // export * as foo from "mod" const starToken = /** @type {Token} */ (tokens.find(isWildcard)) @@ -1302,10 +1303,28 @@ module.exports.defineVisitor = function create( const exportedToken = tokenStore.getTokenAfter(asToken) const afterTokens = tokens.slice(tokens.indexOf(exportedToken) + 1) - setOffset(starToken, 1, firstToken) + setOffset(starToken, 1, exportToken) setOffset(asToken, 1, starToken) setOffset(exportedToken, 1, starToken) - setOffset(afterTokens, 1, firstToken) + setOffset(afterTokens, 1, exportToken) + } + + // assertions + const lastToken = /** @type {Token} */ ( + tokenStore.getLastToken(node, isNotSemicolonToken) + ) + const assertionTokens = tokenStore.getTokensBetween( + node.source, + lastToken + ) + if (assertionTokens.length) { + const assertToken = /** @type {Token} */ (assertionTokens.shift()) + setOffset(assertToken, 0, exportToken) + const assertionOpen = assertionTokens.shift() + if (assertionOpen) { + setOffset(assertionOpen, 1, assertToken) + processNodeList(assertionTokens, assertionOpen, lastToken, 1) + } } }, /** @param {ExportDefaultDeclaration} node */ @@ -1328,28 +1347,66 @@ module.exports.defineVisitor = function create( const firstSpecifier = node.specifiers[0] if (!firstSpecifier || firstSpecifier.type === 'ExportSpecifier') { // export {foo, bar}; or export {foo, bar} from "mod"; - const leftParenToken = tokenStore.getFirstToken(node, 1) - const rightParenToken = /** @type {Token} */ ( - tokenStore.getLastToken(node, isClosingBraceToken) + const leftBraceTokens = firstSpecifier + ? tokenStore.getTokensBetween(exportToken, firstSpecifier) + : [tokenStore.getTokenAfter(exportToken)] + const rightBraceToken = /** @type {Token} */ ( + node.source + ? tokenStore.getTokenBefore(node.source, isClosingBraceToken) + : tokenStore.getLastToken(node, isClosingBraceToken) + ) + setOffset(leftBraceTokens, 0, exportToken) + processNodeList( + node.specifiers, + /** @type {Token} */ (last(leftBraceTokens)), + rightBraceToken, + 1 ) - setOffset(leftParenToken, 0, exportToken) - processNodeList(node.specifiers, leftParenToken, rightParenToken, 1) - - const maybeFromToken = tokenStore.getTokenAfter(rightParenToken) - if (maybeFromToken != null && maybeFromToken.value === 'from') { - const fromToken = maybeFromToken - const nameToken = tokenStore.getTokenAfter(fromToken) - setOffset([fromToken, nameToken], 1, exportToken) + + if (node.source) { + const tokens = tokenStore.getTokensBetween( + rightBraceToken, + node.source + ) + setOffset( + [...tokens, sourceCode.getFirstToken(node.source)], + 1, + exportToken + ) + + // assertions + const lastToken = /** @type {Token} */ ( + tokenStore.getLastToken(node, isNotSemicolonToken) + ) + const assertionTokens = tokenStore.getTokensBetween( + node.source, + lastToken + ) + if (assertionTokens.length) { + const assertToken = /** @type {Token} */ (assertionTokens.shift()) + setOffset(assertToken, 0, exportToken) + const assertionOpen = assertionTokens.shift() + if (assertionOpen) { + setOffset(assertionOpen, 1, assertToken) + processNodeList(assertionTokens, assertionOpen, lastToken, 1) + } + } } } else { // maybe babel parser } } }, - /** @param {ExportSpecifier} node */ - ExportSpecifier(node) { + /** @param {ExportSpecifier | ImportSpecifier} node */ + 'ExportSpecifier, ImportSpecifier'(node) { const tokens = tokenStore.getTokens(node) - const firstToken = /** @type {Token} */ (tokens.shift()) + let firstToken = /** @type {Token} */ (tokens.shift()) + if (firstToken.value === 'type') { + const typeToken = firstToken + firstToken = /** @type {Token} */ (tokens.shift()) + setOffset(firstToken, 0, typeToken) + } + setOffset(tokens, 1, firstToken) }, /** @param {ForInStatement | ForOfStatement} node */ @@ -1540,13 +1597,23 @@ module.exports.defineVisitor = function create( setOffset(fromToken, 1, importToken) setOffset(afterTokens, 0, fromToken) } - }, - /** @param {ImportSpecifier} node */ - ImportSpecifier(node) { - if (node.local.range[0] !== node.imported.range[0]) { - const tokens = tokenStore.getTokens(node) - const firstToken = /** @type {Token} */ (tokens.shift()) - setOffset(tokens, 1, firstToken) + + // assertions + const lastToken = /** @type {Token} */ ( + tokenStore.getLastToken(node, isNotSemicolonToken) + ) + const assertionTokens = tokenStore.getTokensBetween( + node.source, + lastToken + ) + if (assertionTokens.length) { + const assertToken = /** @type {Token} */ (assertionTokens.shift()) + setOffset(assertToken, 0, importToken) + const assertionOpen = assertionTokens.shift() + if (assertionOpen) { + setOffset(assertionOpen, 1, assertToken) + processNodeList(assertionTokens, assertionOpen, lastToken, 1) + } } }, /** @param {ImportNamespaceSpecifier} node */ diff --git a/lib/utils/indent-ts.js b/lib/utils/indent-ts.js index b98e9d105..496e892cd 100644 --- a/lib/utils/indent-ts.js +++ b/lib/utils/indent-ts.js @@ -1302,6 +1302,27 @@ function defineVisitor({ setOffset(atToken, 0, tokenStore.getFirstToken(decorators[0])) } }, + ImportAttribute(node) { + const firstToken = tokenStore.getFirstToken(node) + const keyTokens = getFirstAndLastTokens(node.key) + const prefixTokens = tokenStore.getTokensBetween( + firstToken, + keyTokens.firstToken + ) + setOffset(prefixTokens, 0, firstToken) + + setOffset(keyTokens.firstToken, 0, firstToken) + + const initToken = tokenStore.getFirstToken(node.value) + setOffset( + [ + ...tokenStore.getTokensBetween(keyTokens.lastToken, initToken), + initToken + ], + 1, + keyTokens.lastToken + ) + }, // ---------------------------------------------------------------------- // DEPRECATED NODES diff --git a/package.json b/package.json index 2c9bcbf18..bcc6bfcdd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-vue", - "version": "8.1.1", + "version": "8.2.0", "description": "Official ESLint plugin for Vue.js", "main": "lib/index.js", "scripts": { @@ -65,7 +65,7 @@ "@types/natural-compare": "^1.4.0", "@types/node": "^13.13.5", "@types/semver": "^7.2.0", - "@typescript-eslint/parser": "^5.4.0", + "@typescript-eslint/parser": "^5.5.0", "@vuepress/plugin-pwa": "^1.4.1", "acorn": "^8.5.0", "env-cmd": "^10.1.0", @@ -82,7 +82,7 @@ "mocha": "^7.1.2", "nyc": "^15.1.0", "prettier": "^2.4.1", - "typescript": "^4.5.0-0", + "typescript": "^4.5.0", "vue-eslint-editor": "^1.1.0", "vuepress": "^1.8.2" } diff --git a/tests/fixtures/script-indent/ts-import-assertion-01.vue b/tests/fixtures/script-indent/ts-import-assertion-01.vue new file mode 100644 index 000000000..f14761841 --- /dev/null +++ b/tests/fixtures/script-indent/ts-import-assertion-01.vue @@ -0,0 +1,9 @@ + + diff --git a/tests/fixtures/script-indent/ts-import-assertion-02.vue b/tests/fixtures/script-indent/ts-import-assertion-02.vue new file mode 100644 index 000000000..86fe80b4e --- /dev/null +++ b/tests/fixtures/script-indent/ts-import-assertion-02.vue @@ -0,0 +1,15 @@ + + diff --git a/tests/fixtures/script-indent/ts-import-assertion-03.vue b/tests/fixtures/script-indent/ts-import-assertion-03.vue new file mode 100644 index 000000000..ee88e92ce --- /dev/null +++ b/tests/fixtures/script-indent/ts-import-assertion-03.vue @@ -0,0 +1,10 @@ + + diff --git a/tests/fixtures/script-indent/ts-import-assertion-04.vue b/tests/fixtures/script-indent/ts-import-assertion-04.vue new file mode 100644 index 000000000..76d2f2860 --- /dev/null +++ b/tests/fixtures/script-indent/ts-import-assertion-04.vue @@ -0,0 +1,10 @@ + + diff --git a/tests/fixtures/script-indent/ts-type-only-import-export-01.vue b/tests/fixtures/script-indent/ts-type-only-import-export-01.vue new file mode 100644 index 000000000..97c6dc192 --- /dev/null +++ b/tests/fixtures/script-indent/ts-type-only-import-export-01.vue @@ -0,0 +1,17 @@ + + diff --git a/tests/fixtures/script-indent/ts-type-only-import-export-02.vue b/tests/fixtures/script-indent/ts-type-only-import-export-02.vue new file mode 100644 index 000000000..0ef5f1106 --- /dev/null +++ b/tests/fixtures/script-indent/ts-type-only-import-export-02.vue @@ -0,0 +1,23 @@ + + diff --git a/tests/fixtures/script-indent/ts-type-only-import-export-03.vue b/tests/fixtures/script-indent/ts-type-only-import-export-03.vue new file mode 100644 index 000000000..ca280f03c --- /dev/null +++ b/tests/fixtures/script-indent/ts-type-only-import-export-03.vue @@ -0,0 +1,26 @@ + + diff --git a/tests/lib/rules/component-name-in-template-casing.js b/tests/lib/rules/component-name-in-template-casing.js index baeb66433..4c4d9f5cb 100644 --- a/tests/lib/rules/component-name-in-template-casing.js +++ b/tests/lib/rules/component-name-in-template-casing.js @@ -69,6 +69,10 @@ tester.run('component-name-in-template-casing', rule, { code: '', options: ['PascalCase', { registeredComponentsOnly: false }] }, + { + code: '', + options: ['PascalCase', { registeredComponentsOnly: false }] + }, { code: '', options: ['PascalCase', { registeredComponentsOnly: false }] @@ -151,6 +155,19 @@ tester.run('component-name-in-template-casing', rule, { { code: '