From b19843c698110d9dfb08fe8dffdbd0aaffbcce36 Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 25 Dec 2019 22:27:37 -0500 Subject: [PATCH] =?UTF-8?q?=20=E2=AD=90=EF=B8=8F=20New:=20Add=20rule=20`no?= =?UTF-8?q?-reserved-component-names`=20(#757)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :star: add rule no-reserved-component-names * Increase test coverage * Checking elements against element lists * :hammer: Update PR with ota-meshi suggestions * :hammer: Lint locally registered components * :ok_hand: Adding tests to validate slot and template * :hammer: Linting for deprecated elements --- docs/rules/no-reserved-component-names.md | 28 ++ lib/rules/no-reserved-component-names.js | 125 ++++++ lib/utils/deprecated-html-elements.json | 1 + .../lib/rules/no-reserved-component-names.js | 375 ++++++++++++++++++ 4 files changed, 529 insertions(+) create mode 100644 docs/rules/no-reserved-component-names.md create mode 100644 lib/rules/no-reserved-component-names.js create mode 100644 lib/utils/deprecated-html-elements.json create mode 100644 tests/lib/rules/no-reserved-component-names.js diff --git a/docs/rules/no-reserved-component-names.md b/docs/rules/no-reserved-component-names.md new file mode 100644 index 000000000..57a008592 --- /dev/null +++ b/docs/rules/no-reserved-component-names.md @@ -0,0 +1,28 @@ +# vue/no-reserved-component-names +> disallow the use of reserved names in component definitions + +- :gear: This rule is included in all of `"plugin:vue/essential"`, `"plugin:vue/recommended"`, and `"plugin:vue/strongly-recommended"`. + +## :book: Rule Details + +This rule prevents name collisions between vue components and standard html elements. + + + +```vue + +``` + + + +## :books: Further reading + +- [List of html elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Element) +- [List of SVG elements](https://developer.mozilla.org/en-US/docs/Web/SVG/Element) +- [Kebab case elements](https://stackoverflow.com/questions/22545621/do-custom-elements-require-a-dash-in-their-name/22545622#22545622) +- [Valid custom element name](https://w3c.github.io/webcomponents/spec/custom/#valid-custom-element-name) \ No newline at end of file diff --git a/lib/rules/no-reserved-component-names.js b/lib/rules/no-reserved-component-names.js new file mode 100644 index 000000000..c08b34f5d --- /dev/null +++ b/lib/rules/no-reserved-component-names.js @@ -0,0 +1,125 @@ +/** + * @fileoverview disallow the use of reserved names in component definitions + * @author Jake Hassel + */ +'use strict' + +const utils = require('../utils') +const casing = require('../utils/casing') + +const htmlElements = require('../utils/html-elements.json') +const deprecatedHtmlElements = require('../utils/deprecated-html-elements.json') +const svgElements = require('../utils/svg-elements.json') + +const kebabCaseElements = [ + 'annotation-xml', + 'color-profile', + 'font-face', + 'font-face-src', + 'font-face-uri', + 'font-face-format', + 'font-face-name', + 'missing-glyph' +] + +const isLowercase = (word) => /^[a-z]*$/.test(word) +const capitalizeFirstLetter = (word) => word[0].toUpperCase() + word.substring(1, word.length) + +const RESERVED_NAMES = new Set( + [ + ...kebabCaseElements, + ...kebabCaseElements.map(casing.pascalCase), + ...htmlElements, + ...htmlElements.map(capitalizeFirstLetter), + ...deprecatedHtmlElements, + ...deprecatedHtmlElements.map(capitalizeFirstLetter), + ...svgElements, + ...svgElements.filter(isLowercase).map(capitalizeFirstLetter) + ]) + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow the use of reserved names in component definitions', + category: undefined, // 'essential' + url: 'https://eslint.vuejs.org/rules/no-reserved-component-names.html' + }, + fixable: null, + schema: [] + }, + + create (context) { + function canVerify (node) { + return node.type === 'Literal' || ( + node.type === 'TemplateLiteral' && + node.expressions.length === 0 && + node.quasis.length === 1 + ) + } + + function reportIfInvalid (node) { + let name + if (node.type === 'TemplateLiteral') { + const quasis = node.quasis[0] + name = quasis.value.cooked + } else { + name = node.value + } + if (RESERVED_NAMES.has(name)) { + report(node, name) + } + } + + function report (node, name) { + context.report({ + node: node, + message: 'Name "{{name}}" is reserved.', + data: { + name: name + } + }) + } + + return Object.assign({}, + { + "CallExpression > MemberExpression > Identifier[name='component']" (node) { + const parent = node.parent.parent + const calleeObject = utils.unwrapTypes(parent.callee.object) + + if (calleeObject.type === 'Identifier' && + calleeObject.name === 'Vue' && + parent.arguments && + parent.arguments.length === 2 + ) { + const argument = parent.arguments[0] + + if (canVerify(argument)) { + reportIfInvalid(argument) + } + } + } + }, + utils.executeOnVue(context, (obj) => { + // Report if a component has been registered locally with a reserved name. + utils.getRegisteredComponents(obj) + .filter(({ name }) => RESERVED_NAMES.has(name)) + .forEach(({ node, name }) => report(node, name)) + + const node = obj.properties + .find(item => ( + item.type === 'Property' && + item.key.name === 'name' && + canVerify(item.value) + )) + + if (!node) return + reportIfInvalid(node.value) + }) + ) + } +} diff --git a/lib/utils/deprecated-html-elements.json b/lib/utils/deprecated-html-elements.json new file mode 100644 index 000000000..daf23f512 --- /dev/null +++ b/lib/utils/deprecated-html-elements.json @@ -0,0 +1 @@ +["acronym","applet","basefont","bgsound","big","blink","center","command","content","dir","element","font","frame","frameset","image","isindex","keygen","listing","marquee","menuitem","multicol","nextid","nobr","noembed","noframes","plaintext","shadow","spacer","strike","tt","xmp"] \ No newline at end of file diff --git a/tests/lib/rules/no-reserved-component-names.js b/tests/lib/rules/no-reserved-component-names.js new file mode 100644 index 000000000..6d592396d --- /dev/null +++ b/tests/lib/rules/no-reserved-component-names.js @@ -0,0 +1,375 @@ +/** + * @fileoverview disallow the use of reserved names in component definitions + * @author Jake Hassel + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/no-reserved-component-names') +const RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const invalidElements = [ + 'annotation-xml', 'AnnotationXml', + 'color-profile', 'ColorProfile', + 'font-face', 'FontFace', + 'font-face-src', 'FontFaceSrc', + 'font-face-uri', 'FontFaceUri', + 'font-face-format', 'FontFaceFormat', + 'font-face-name', 'FontFaceName', + 'missing-glyph', 'MissingGlyph', + 'html', 'Html', + 'body', 'Body', + 'base', 'Base', + 'head', 'Head', + 'link', 'Link', + 'meta', 'Meta', + 'style', 'Style', + 'title', 'Title', + 'address', 'Address', + 'article', 'Article', + 'aside', 'Aside', + 'footer', 'Footer', + 'header', 'Header', + 'h1', 'H1', + 'h2', 'H2', + 'h3', 'H3', + 'h4', 'H4', + 'h5', 'H5', + 'h6', 'H6', + 'hgroup', 'Hgroup', + 'nav', 'Nav', + 'section', 'Section', + 'div', 'Div', + 'dd', 'Dd', + 'dl', 'Dl', + 'dt', 'Dt', + 'figcaption', 'Figcaption', + 'figure', 'Figure', + 'hr', 'Hr', + 'img', 'Img', + 'li', 'Li', + 'main', 'Main', + 'ol', 'Ol', + 'p', 'P', + 'pre', 'Pre', + 'ul', 'Ul', + 'a', 'A', + 'b', 'B', + 'abbr', 'Abbr', + 'bdi', 'Bdi', + 'bdo', 'Bdo', + 'br', 'Br', + 'cite', 'Cite', + 'code', 'Code', + 'data', 'Data', + 'dfn', 'Dfn', + 'em', 'Em', + 'i', 'I', + 'kbd', 'Kbd', + 'mark', 'Mark', + 'q', 'Q', + 'rp', 'Rp', + 'rt', 'Rt', + 'rtc', 'Rtc', + 'ruby', 'Ruby', + 's', 'S', + 'samp', 'Samp', + 'small', 'Small', + 'span', 'Span', + 'strong', 'Strong', + 'sub', 'Sub', + 'sup', 'Sup', + 'time', 'Time', + 'u', 'U', + 'var', 'Var', + 'wbr', 'Wbr', + 'area', 'Area', + 'audio', 'Audio', + 'map', 'Map', + 'track', 'Track', + 'video', 'Video', + 'embed', 'Embed', + 'object', 'Object', + 'param', 'Param', + 'source', 'Source', + 'canvas', 'Canvas', + 'script', 'Script', + 'noscript', 'Noscript', + 'del', 'Del', + 'ins', 'Ins', + 'caption', 'Caption', + 'col', 'Col', + 'colgroup', 'Colgroup', + 'table', 'Table', + 'thead', 'Thead', + 'tbody', 'Tbody', + 'tfoot', 'Tfoot', + 'td', 'Td', + 'th', 'Th', + 'tr', 'Tr', + 'button', 'Button', + 'datalist', 'Datalist', + 'fieldset', 'Fieldset', + 'form', 'Form', + 'input', 'Input', + 'label', 'Label', + 'legend', 'Legend', + 'meter', 'Meter', + 'optgroup', 'Optgroup', + 'option', 'Option', + 'output', 'Output', + 'progress', 'Progress', + 'select', 'Select', + 'textarea', 'Textarea', + 'details', 'Details', + 'dialog', 'Dialog', + 'menu', 'Menu', + 'menuitem', 'menuitem', + 'summary', 'Summary', + 'content', 'Content', + 'element', 'Element', + 'shadow', 'Shadow', + 'template', 'Template', + 'slot', 'Slot', + 'blockquote', 'Blockquote', + 'iframe', 'Iframe', + 'noframes', 'Noframes', + 'picture', 'Picture', + + // SVG elements + 'animate', 'Animate', + 'animateMotion', + 'animateTransform', + 'circle', 'Circle', + 'clipPath', + 'defs', 'Defs', + 'desc', 'Desc', + 'discard', 'Discard', + 'ellipse', 'Ellipse', + 'feBlend', + 'feColorMatrix', + 'feComponentTransfer', + 'feComposite', + 'feConvolveMatrix', + 'feDiffuseLighting', + 'feDisplacementMap', + 'feDistantLight', + 'feDropShadow', + 'feFlood', + 'feFuncA', + 'feFuncB', + 'feFuncG', + 'feFuncR', + 'feGaussianBlur', + 'feImage', + 'feMerge', + 'feMergeNode', + 'feMorphology', + 'feOffset', + 'fePointLight', + 'feSpecularLighting', + 'feSpotLight', + 'feTile', + 'feTurbulence', + 'filter', 'Filter', + 'foreignObject', + 'g', 'G', + 'image', 'Image', + 'line', 'Line', + 'linearGradient', + 'marker', 'Marker', + 'mask', 'Mask', + 'metadata', 'Metadata', + 'mpath', 'Mpath', + 'path', 'Path', + 'pattern', 'Pattern', + 'polygon', 'Polygon', + 'polyline', 'Polyline', + 'radialGradient', + 'rect', 'Rect', + 'set', 'Set', + 'stop', 'Stop', + 'svg', 'Svg', + 'switch', 'Switch', + 'symbol', 'Symbol', + 'text', 'Text', + 'textPath', + 'tspan', 'Tspan', + 'unknown', 'Unknown', + 'use', 'Use', + 'view', 'View', + + // Deprecated + 'acronym', 'Acronym', + 'applet', 'Applet', + 'basefont', 'Basefont', + 'bgsound', 'Bgsound', + 'big', 'Big', + 'blink', 'Blink', + 'center', 'Center', + 'command', 'Command', + 'dir', 'Dir', + 'font', 'Font', + 'frame', 'Frame', + 'frameset', 'Frameset', + 'isindex', 'Isindex', + 'keygen', 'Keygen', + 'listing', 'Listing', + 'marquee', 'Marquee', + 'multicol', 'Multicol', + 'nextid', 'Nextid', + 'nobr', 'Nobr', + 'noembed', 'Noembed', + 'plaintext', 'Plaintext', + 'spacer', 'Spacer', + 'strike', 'Strike', + 'tt', 'Tt', + 'xmp', 'Xmp' +] + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module' +} + +const ruleTester = new RuleTester() +ruleTester.run('no-reserved-component-names', rule, { + + valid: [ + { + filename: 'test.vue', + code: ` + export default { + } + `, + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + ...name + } + `, + parserOptions + }, + { + filename: 'test.vue', + code: ` + export default { + name: 'FooBar' + } + `, + parserOptions + }, + { + filename: 'test.vue', + code: `Vue.component('FooBar', {})`, + parserOptions + }, + { + filename: 'test.js', + code: ` + new Vue({ + name: 'foo!bar' + }) + `, + parserOptions + }, + { + filename: 'test.vue', + code: `Vue.component(\`fooBar\${foo}\`, component)`, + parserOptions + }, + { + filename: 'test.vue', + code: ` + + `, + parser: 'vue-eslint-parser', + parserOptions + }, + { + filename: 'test.vue', + code: ` + + `, + parser: 'vue-eslint-parser', + parserOptions + } + ], + + invalid: [ + ...invalidElements.map(name => { + return { + filename: `${name}.vue`, + code: ` + export default { + name: '${name}' + } + `, + parserOptions, + errors: [{ + message: `Name "${name}" is reserved.`, + type: 'Literal', + line: 3 + }] + } + }), + ...invalidElements.map(name => { + return { + filename: 'test.vue', + code: `Vue.component('${name}', component)`, + parserOptions, + errors: [{ + message: `Name "${name}" is reserved.`, + type: 'Literal', + line: 1 + }] + } + }), + ...invalidElements.map(name => { + return { + filename: 'test.vue', + code: `Vue.component(\`${name}\`, {})`, + parserOptions, + errors: [{ + message: `Name "${name}" is reserved.`, + type: 'TemplateLiteral', + line: 1 + }] + } + }), + ...invalidElements.map(name => { + return { + filename: 'test.vue', + code: `export default { + components: { + '${name}': {}, + } + }`, + parserOptions, + errors: [{ + message: `Name "${name}" is reserved.`, + type: 'Property', + line: 3 + }] + } + }) + ] +})