From 5111589e4679c87e1808cb5b92ed197cec898473 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Wed, 24 Mar 2021 18:35:15 +0000 Subject: [PATCH 1/7] feat: add tag-names library file --- lib/tag-names.js | 155 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 lib/tag-names.js diff --git a/lib/tag-names.js b/lib/tag-names.js new file mode 100644 index 0000000..1e4965c --- /dev/null +++ b/lib/tag-names.js @@ -0,0 +1,155 @@ +module.exports.knownNamespaces = new Set([ + 'primer', + 'polymer', + 'x', + 'rdf', + 'xml', + 'annotation', + 'color', + 'font', + 'missing', + + // https://www.webcomponents.org/collections + 'ct', + 'color', + 'currency', + 'smart', + 'github', + 'elementui', + 'smart', + 'github', + 'elementui', + 'progressive', + 'cpss', + 'xgd', + 'paper', + 'hy', + 'polymorph', + 'granite', + 'plastic', + 'vega', + 'catalyst', + 'Gathr', + 'fs', + 'cr', + 'mqtt', + 'simpla', + 'iron', + 'topseed', + 'awesome', + 'ts', + 'globalization', + 'super', + 's', + 'scary', + 'best', + 'paper', + 'platinum', + 'paper', + 'layout', + 'iron', + 'app', + 'data', + 'gold', + 'elements', + 'pf', + 'paperfire', + 'core', + 'nodecg', + 'titanium', + 'lrndesign', + 'lrn', + 'hax', + 'k4ng', + 'ros', + 'geo', + 'ibm', + 'google', + 'gfs', + 'geoloeg', + 'ami', + 'google', + 'medical', + 'cordova', + 'cth', + 'paper', + 'gui', + 'network', + 'dialogs', + 'layouts', + + // https://github.com/nuxodin/web-namespace-registry/ + 'lume', + 'three', + 'fast', + 'a11y', + 'lrn', + 'mdl', + 'mui', + 'pure', + 'uk', + 'auro', + 'ui5', + 's', + 'ng', + 'amp', + 'spectrum', + 'sp', + 'smart', + 'hiq', + 'ibm', + 'google', + 'a', + 'tangy', + 'bs', + 'ion', + 'mwc', + 'mdc', + 'mat', + 'mv', + 'iron', + 'paper', + 'gold', + 'std', + 'aria', + 'dile', + 'lion' +]) + +module.exports.reservedTags = new Set([ + 'annotation-xml', + 'color-profile', + 'font-face', + 'font-face-src', + 'font-face-uri', + 'font-face-format', + 'font-face-name', + 'missing-glyph' +]) + +// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name +const PCENChar = [ + '[', + '-', + '\\.', + '0-9', + '_', + 'a-z', + '\\u00B7', // 路 + '\\u00C0-\\u00D6', + '\\u00D8-\\u00F6', + '\\u00F8-\\u037D', + '\\u037F-\\u1FFF', + '\\u200C-\\u200D', + '\\u203F-\\u2040', + '\\u2070-\\u218F', + '\\u2C00-\\u2FEF', + '\\u3001-\\uD7FF', + '\\uF900-\\uFDCF', + '\\uFDF0-\\uFFFD', + '\\u10000-\\uEFFFF', + ']' +].join('') +const customElementNameRegExp = new RegExp(`^[a-z]${PCENChar}*-${PCENChar}*$`) + +module.exports.validTagName = name => customElementNameRegExp.test(name) From adff8d3109272a07b27a1f2db6492aa73a5546f3 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Wed, 24 Mar 2021 18:39:31 +0000 Subject: [PATCH 2/7] feat: add valid-tag-name rule --- README.md | 1 + docs/rules/valid-tag-name.md | 39 ++++++++++ lib/rules.js | 3 +- lib/rules/valid-tag-name.js | 64 +++++++++++++++ test/valid-tag-name.js | 147 +++++++++++++++++++++++++++++++++++ 5 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 docs/rules/valid-tag-name.md create mode 100644 lib/rules/valid-tag-name.js create mode 100644 test/valid-tag-name.js diff --git a/README.md b/README.md index 5cb02e1..3beffcc 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,4 @@ JSON ESLint config example: ### Rules - [Define Tag After Class Definition](./docs/rules/define-tag-after-class-definition.md) + - [Valid Tag Name](./docs/rules/valid-tag-name.md) diff --git a/docs/rules/valid-tag-name.md b/docs/rules/valid-tag-name.md new file mode 100644 index 0000000..2f5faf3 --- /dev/null +++ b/docs/rules/valid-tag-name.md @@ -0,0 +1,39 @@ +# Valid Tag Name + +To be able to use a Custom Element, it must be defined in the `customElements` registry with a valid tag name. Certain characters are allowed for tag names, and invalid tag-names will throw a `DOMException` during runtime. + +## Rule Details + +This rule enforces that calls to `customElements.define` or `customElements.whenDefined` express a valid tag name. There are certain mandatory rules a tag name must follow - such as starting with a letter, and including one dash. This rule will check the name follows these rules. + +In addition, this rule can be customised to perform additional checks, such as requiring a prefix, or disallowing known prefixes such as `x-`. + +馃憥 Examples of **incorrect** code for this rule: + +```js +customElements.define('foobar', ...) +``` + +馃憤 Examples of **correct** code for this rule: + +```js +customElements.define('foo-bar', ...) +``` + +### Options + + - `onlyAlphanum` is a boolean option (default: `false`). When set to `true` it will only allow tag names to consist of alphanumeric characters from a-z, 0-9. The spec allows for a broad range of additional characters such as some emoji, which will be disallowed if this rule is set to `true`. + + - `disallowNamespaces` is a boolean option (default: `false`). When set to `true` it will check the tag does not match an existing well known namespace, such as `x-` or `google-`, etc. Setting this rule to true limits the risk of collision with other third-party element names. + + - `suffix` can be set to one or more strings, and elements must use at least one of these suffixes. + + - `prefix` can be set to one or more strings, and elements must use at least one of these prefixes. + +## When Not To Use It + +If you do not want to validate the names passed to `customElements.define` or `customElements.whenDefined` then you can disable this rule. + +## Version + +This was added in v0.0.1 diff --git a/lib/rules.js b/lib/rules.js index c70cc4f..d889c9c 100644 --- a/lib/rules.js +++ b/lib/rules.js @@ -1,3 +1,4 @@ module.exports = { - 'define-tag-after-class-definition': require('./rules/define-tag-after-class-definition') + 'define-tag-after-class-definition': require('./rules/define-tag-after-class-definition'), + 'valid-tag-name': require('./rules/valid-tag-name') } diff --git a/lib/rules/valid-tag-name.js b/lib/rules/valid-tag-name.js new file mode 100644 index 0000000..a14bca8 --- /dev/null +++ b/lib/rules/valid-tag-name.js @@ -0,0 +1,64 @@ +const s = require('../custom-selectors') +const {validTagName, reservedTags, knownNamespaces} = require('../tag-names') +module.exports = { + meta: { + type: 'problem', + docs: {description: '', url: require('../url')(module)} + }, + schema: [ + { + type: 'object', + properties: { + disallowNamespaces: {type: 'boolean'}, + onlyAlphanum: {type: 'boolean'}, + prefix: { + oneOf: [{type: 'string'}, {type: 'array', items: {type: 'string'}}] + }, + suffix: { + oneOf: [{type: 'string'}, {type: 'array', items: {type: 'string'}}] + } + } + } + ], + create(context) { + return { + [s.customElements.define](node) { + const nameArg = node.arguments[0] + let name = nameArg.value + if (nameArg.type === 'TemplateLiteral') { + // Give up on TemplateLiteral expressions + if (nameArg.expressions.length) return + name = nameArg.quasis.map(q => q.value.raw).join('') + } + const {disallowNamespaces, onlyAlphanum} = context.options[0] || {} + let {prefix, suffix} = context.options[0] || {} + suffix = [].concat(suffix || []) + prefix = [].concat(prefix || []) + const namespace = (name.match(/^(.*)-/) || [])[1] + if (!/-/.test(name)) { + context.report(nameArg, 'Custom Element names must contain at least one dash (-)') + } else if (/^-/.test(name)) { + context.report(nameArg, 'Custom Element names must not start with a dash (-)') + } else if (/^<.*>$/.test(name)) { + context.report(nameArg, 'Custom Element names must not include the tag syntax (<>)') + } else if (/[A-Z]/.test(name)) { + context.report(nameArg, 'Custom Element names must not contain capital letters') + } else if (!/^[a-z]/.test(name)) { + context.report(nameArg, 'Custom Element names must start with a letter') + } else if (reservedTags.has(name)) { + context.report(nameArg, `Custom Elements cannot be given the reserved name "${name}"`) + } else if (!validTagName(name)) { + context.report(nameArg, `${name} is not a valid custom element name`) + } else if (onlyAlphanum && !/^[a-z0-9-]+$/.test(name)) { + context.report(nameArg, `Non ASCII Custom Elements have been disallowed`) + } else if (disallowNamespaces && knownNamespaces.has(namespace) && !prefix.includes(namespace)) { + context.report(nameArg, `${namespace} is a common namespace, and has been disallowed`) + } else if (suffix.length && !suffix.some(end => name.endsWith(end))) { + context.report(nameArg, `Custom Element name must end with a suffix: ${suffix}`) + } else if (prefix.length && !prefix.some(start => name.startsWith(start))) { + context.report(nameArg, `Custom Element name must begin with a prefix: ${prefix}`) + } + } + } + } +} diff --git a/test/valid-tag-name.js b/test/valid-tag-name.js new file mode 100644 index 0000000..530c62a --- /dev/null +++ b/test/valid-tag-name.js @@ -0,0 +1,147 @@ +const rule = require('../lib/rules/valid-tag-name') +const RuleTester = require('eslint').RuleTester + +const ruleTester = new RuleTester({env: {es2020: true}}) + +ruleTester.run('valid-tag-name', rule, { + valid: [ + {code: 'customElements.define("foo-bar")'}, + {code: "customElements.define('foo-bar')"}, + {code: 'customElements.define(`foo-bar`)'}, + {code: 'customElements.define("foo-bar")'}, + {code: 'customElements.define("m---路路路---")'}, + {code: 'customElements.define("a脌-")'}, + {code: 'customElements.define("lei冒inlegt-t铆st")'}, + {code: 'customElements.define("a-馃樁鈥嶐煂笍")'}, + {code: 'customElements.define("notice-漏")'}, + {code: 'customElements.define("a-b-c-")', options: [{onlyAlphanum: true}]}, + {code: 'customElements.define("foo-bar")', options: [{disallowNamespaces: true}]}, + {code: 'customElements.define("ng-bar")', options: [{disallowNamespaces: true, prefix: 'ng'}]} + ], + invalid: [ + { + code: 'customElements.define("foo")', + errors: [ + { + message: 'Custom Element names must contain at least one dash (-)', + type: 'Literal' + } + ] + }, + { + code: 'customElements.define("-foo-bar")', + errors: [ + { + message: 'Custom Element names must not start with a dash (-)', + type: 'Literal' + } + ] + }, + { + code: 'customElements.define("foo-BAR")', + errors: [ + { + message: 'Custom Element names must not contain capital letters', + type: 'Literal' + } + ] + }, + { + code: 'customElements.define("")', + errors: [ + { + message: 'Custom Element names must not include the tag syntax (<>)', + type: 'Literal' + } + ] + }, + { + code: 'customElements.define("911-emergency")', + errors: [ + { + message: 'Custom Element names must start with a letter', + type: 'Literal' + } + ] + }, + { + code: 'customElements.define("annotation-xml")', + errors: [ + { + message: 'Custom Elements cannot be given the reserved name "annotation-xml"', + type: 'Literal' + } + ] + }, + { + code: 'customElements.define("a-馃樁鈥嶐煂笍")', + options: [ + { + onlyAlphanum: true + } + ], + errors: [ + { + message: 'Non ASCII Custom Elements have been disallowed', + type: 'Literal' + } + ] + }, + { + code: 'customElements.define("a-馃樁鈥嶐煂笍")', + options: [ + { + onlyAlphanum: true + } + ], + errors: [ + { + message: 'Non ASCII Custom Elements have been disallowed', + type: 'Literal' + } + ] + }, + { + code: 'customElements.define("ng-element")', + options: [ + { + disallowNamespaces: true + } + ], + errors: [ + { + message: 'ng is a common namespace, and has been disallowed', + type: 'Literal' + } + ] + }, + { + code: 'customElements.define("abc-element")', + options: [ + { + prefix: 'foo' + } + ], + errors: [ + { + message: 'Custom Element name must begin with a prefix: foo', + type: 'Literal' + } + ] + }, + { + code: 'customElements.define("abc-component")', + options: [ + { + suffix: 'element' + } + ], + errors: [ + { + message: 'Custom Element name must end with a suffix: element', + type: 'Literal' + } + ] + } + ] +}) From b2862a58a0f2319166926892f5aebb74d9bf6c21 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Thu, 25 Mar 2021 14:42:02 +0000 Subject: [PATCH 3/7] =?UTF-8?q?test(valid-tag-name):=20notice-=C2=A9=20is?= =?UTF-8?q?=20not=20a=20valid=20tag=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/valid-tag-name.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/valid-tag-name.js b/test/valid-tag-name.js index 530c62a..8f63566 100644 --- a/test/valid-tag-name.js +++ b/test/valid-tag-name.js @@ -13,7 +13,6 @@ ruleTester.run('valid-tag-name', rule, { {code: 'customElements.define("a脌-")'}, {code: 'customElements.define("lei冒inlegt-t铆st")'}, {code: 'customElements.define("a-馃樁鈥嶐煂笍")'}, - {code: 'customElements.define("notice-漏")'}, {code: 'customElements.define("a-b-c-")', options: [{onlyAlphanum: true}]}, {code: 'customElements.define("foo-bar")', options: [{disallowNamespaces: true}]}, {code: 'customElements.define("ng-bar")', options: [{disallowNamespaces: true, prefix: 'ng'}]} @@ -64,6 +63,15 @@ ruleTester.run('valid-tag-name', rule, { } ] }, + { + code: 'customElements.define("notice-漏")', + errors: [ + { + message: 'notice-漏 is not a valid custom element name', + type: 'Literal' + } + ] + }, { code: 'customElements.define("annotation-xml")', errors: [ From 045a325a1ded5bcfe26ffc481b4b297ffd69c379 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Thu, 25 Mar 2021 14:42:43 +0000 Subject: [PATCH 4/7] refactor(tag-names): fix broken PCENChar regexp --- lib/tag-names.js | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/tag-names.js b/lib/tag-names.js index 1e4965c..25cffd6 100644 --- a/lib/tag-names.js +++ b/lib/tag-names.js @@ -129,27 +129,27 @@ module.exports.reservedTags = new Set([ // https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name const PCENChar = [ - '[', - '-', + '(?:\\-', '\\.', - '0-9', + '[0-9]', '_', - 'a-z', - '\\u00B7', // 路 - '\\u00C0-\\u00D6', - '\\u00D8-\\u00F6', - '\\u00F8-\\u037D', - '\\u037F-\\u1FFF', - '\\u200C-\\u200D', - '\\u203F-\\u2040', - '\\u2070-\\u218F', - '\\u2C00-\\u2FEF', - '\\u3001-\\uD7FF', - '\\uF900-\\uFDCF', - '\\uFDF0-\\uFFFD', - '\\u10000-\\uEFFFF', - ']' -].join('') + '[a-z]', + '\\xB7', // 路 + '[\\u00C0-\\u00D6]', + '[\\u00D8-\\u00F6]', + '[\\u00F8-\\u037D]', + '[\\u037F-\\u1FFF]', + '[\\u200C-\\u200D]', + '[\\u203F-\\u2040]', + '[\\u2070-\\u218F]', + '[\\u2C00-\\u2FEF]', + '[\\u3001-\\uD7FF]', + '[\\uF900-\\uFDCF]', + '[\\uFDF0-\\uFFFD]', + '[\\uD800-\\uDB7F]', + '[\\uDC00-\\uDFFF])' +].join('|') +module.exports.PCENChar = PCENChar const customElementNameRegExp = new RegExp(`^[a-z]${PCENChar}*-${PCENChar}*$`) module.exports.validTagName = name => customElementNameRegExp.test(name) From 6298c0b4552ff5203b01a7295e52b426dc05c62d Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Thu, 25 Mar 2021 14:43:20 +0000 Subject: [PATCH 5/7] refactor(valid-tag-name): utilise hints in error message to better report violations --- lib/rules/valid-tag-name.js | 36 +++++++++++++++++++----------------- test/valid-tag-name.js | 10 +++++----- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/lib/rules/valid-tag-name.js b/lib/rules/valid-tag-name.js index a14bca8..77370db 100644 --- a/lib/rules/valid-tag-name.js +++ b/lib/rules/valid-tag-name.js @@ -35,27 +35,29 @@ module.exports = { suffix = [].concat(suffix || []) prefix = [].concat(prefix || []) const namespace = (name.match(/^(.*)-/) || [])[1] - if (!/-/.test(name)) { - context.report(nameArg, 'Custom Element names must contain at least one dash (-)') - } else if (/^-/.test(name)) { - context.report(nameArg, 'Custom Element names must not start with a dash (-)') - } else if (/^<.*>$/.test(name)) { - context.report(nameArg, 'Custom Element names must not include the tag syntax (<>)') - } else if (/[A-Z]/.test(name)) { - context.report(nameArg, 'Custom Element names must not contain capital letters') - } else if (!/^[a-z]/.test(name)) { - context.report(nameArg, 'Custom Element names must start with a letter') - } else if (reservedTags.has(name)) { + if (!validTagName(name)) { + const hints = [] + if (!/^[a-z]/.test(name)) hints.push('Custom Element names must start with a letter') + if (!/-/.test(name)) hints.push('Custom Element names must contain at least one dash (-)') + if (/[A-Z]/.test(name)) hints.push('Custom Element names must not contain capital letters') + context.report( + nameArg, + `${name} is not a valid custom element name${hints.length ? '\n' : ''}${hints.join('\n')}` + ) + } + if (reservedTags.has(name)) { context.report(nameArg, `Custom Elements cannot be given the reserved name "${name}"`) - } else if (!validTagName(name)) { - context.report(nameArg, `${name} is not a valid custom element name`) - } else if (onlyAlphanum && !/^[a-z0-9-]+$/.test(name)) { + } + if (onlyAlphanum && !/^[a-z0-9-]+$/.test(name)) { context.report(nameArg, `Non ASCII Custom Elements have been disallowed`) - } else if (disallowNamespaces && knownNamespaces.has(namespace) && !prefix.includes(namespace)) { + } + if (disallowNamespaces && knownNamespaces.has(namespace) && !prefix.includes(namespace)) { context.report(nameArg, `${namespace} is a common namespace, and has been disallowed`) - } else if (suffix.length && !suffix.some(end => name.endsWith(end))) { + } + if (suffix.length && !suffix.some(end => name.endsWith(end))) { context.report(nameArg, `Custom Element name must end with a suffix: ${suffix}`) - } else if (prefix.length && !prefix.some(start => name.startsWith(start))) { + } + if (prefix.length && !prefix.some(start => name.startsWith(start))) { context.report(nameArg, `Custom Element name must begin with a prefix: ${prefix}`) } } diff --git a/test/valid-tag-name.js b/test/valid-tag-name.js index 8f63566..c4fdb9b 100644 --- a/test/valid-tag-name.js +++ b/test/valid-tag-name.js @@ -22,7 +22,7 @@ ruleTester.run('valid-tag-name', rule, { code: 'customElements.define("foo")', errors: [ { - message: 'Custom Element names must contain at least one dash (-)', + message: 'foo is not a valid custom element name\nCustom Element names must contain at least one dash (-)', type: 'Literal' } ] @@ -31,7 +31,7 @@ ruleTester.run('valid-tag-name', rule, { code: 'customElements.define("-foo-bar")', errors: [ { - message: 'Custom Element names must not start with a dash (-)', + message: '-foo-bar is not a valid custom element name\nCustom Element names must start with a letter', type: 'Literal' } ] @@ -40,7 +40,7 @@ ruleTester.run('valid-tag-name', rule, { code: 'customElements.define("foo-BAR")', errors: [ { - message: 'Custom Element names must not contain capital letters', + message: 'foo-BAR is not a valid custom element name\nCustom Element names must not contain capital letters', type: 'Literal' } ] @@ -49,7 +49,7 @@ ruleTester.run('valid-tag-name', rule, { code: 'customElements.define("")', errors: [ { - message: 'Custom Element names must not include the tag syntax (<>)', + message: ' is not a valid custom element name\nCustom Element names must start with a letter', type: 'Literal' } ] @@ -58,7 +58,7 @@ ruleTester.run('valid-tag-name', rule, { code: 'customElements.define("911-emergency")', errors: [ { - message: 'Custom Element names must start with a letter', + message: '911-emergency is not a valid custom element name\nCustom Element names must start with a letter', type: 'Literal' } ] From 2c1a6377b492ed5204a7e9dc59903bdf6be3f5d8 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Thu, 25 Mar 2021 14:57:23 +0000 Subject: [PATCH 6/7] docs(valid-tag-name): fix whitespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kristj谩n Oddsson --- docs/rules/valid-tag-name.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/rules/valid-tag-name.md b/docs/rules/valid-tag-name.md index 2f5faf3..ae7065e 100644 --- a/docs/rules/valid-tag-name.md +++ b/docs/rules/valid-tag-name.md @@ -23,11 +23,8 @@ customElements.define('foo-bar', ...) ### Options - `onlyAlphanum` is a boolean option (default: `false`). When set to `true` it will only allow tag names to consist of alphanumeric characters from a-z, 0-9. The spec allows for a broad range of additional characters such as some emoji, which will be disallowed if this rule is set to `true`. - - `disallowNamespaces` is a boolean option (default: `false`). When set to `true` it will check the tag does not match an existing well known namespace, such as `x-` or `google-`, etc. Setting this rule to true limits the risk of collision with other third-party element names. - - `suffix` can be set to one or more strings, and elements must use at least one of these suffixes. - - `prefix` can be set to one or more strings, and elements must use at least one of these prefixes. ## When Not To Use It From d0df3edd575bd9fac347c489f68dfd69bc06beae Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Fri, 26 Mar 2021 16:52:20 +0000 Subject: [PATCH 7/7] docs(readme): fix merge error --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 9b49185..c2a6163 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,10 @@ JSON ESLint config example: ### Rules -- [Define Tag After Class Definition](./docs/rules/define-tag-after-class-definition.md) - [Define Tag After Class Definition](./docs/rules/define-tag-after-class-definition.md) - [Expose Class on Global](./docs/rules/expose-class-on-global.md) - [File Name Matches Element](./docs/rules/file-name-matches-element.md) - [No Constructor](./docs/rules/no-constructor.md) - [No Customized Built in Elements](./docs/rules/no-customized-built-in-elements.md) - [One Element Per File](./docs/rules/one-element-per-file.md) -- [Valid Tag Name](./docs/rules/valid-tag-name.md) \ No newline at end of file +- [Valid Tag Name](./docs/rules/valid-tag-name.md)