From 7ef99c414874b1fac4f6bf0e333815e331f67d6f Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Wed, 24 Mar 2021 18:32:25 +0000 Subject: [PATCH 1/6] feat: add tag-names library file --- lib/tag-names.js | 128 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 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..833d96b --- /dev/null +++ b/lib/tag-names.js @@ -0,0 +1,128 @@ +module.exports.builtInTagMap = { + a: 'HTMLAnchorElement', + abbr: 'HTMLElement', + address: 'HTMLElement', + area: 'HTMLAreaElement', + article: 'HTMLElement', + aside: 'HTMLElement', + audio: 'HTMLAudioElement', + b: 'HTMLElement', + base: 'HTMLBaseElement', + basefont: 'HTMLBaseFontElement', + bdi: 'HTMLElement', + bdo: 'HTMLElement', + bgsound: 'HTMLBGSoundElement', + blink: 'HTMLPhraseElement', + blockquote: 'HTMLQuoteElement', + body: 'HTMLBodyElement', + br: 'HTMLBRElement', + button: 'HTMLButtonElement', + canvas: 'HTMLCanvasElement', + caption: 'HTMLTableCaptionElement', + cite: 'HTMLPhraseElement', + code: 'HTMLElement', + col: 'HTMLTableColElement', + colgroup: 'HTMLTableColElement', + content: 'HTMLContentElement', + data: 'HTMLDataElement', + datalist: 'HTMLDataListElement', + dd: 'HTMLElement', + del: 'HTMLModElement', + details: 'HTMLDetailsElement', + dfn: 'HTMLElement', + dialog: 'HTMLDialogElement', + dir: 'HTMLDirectoryElement', + div: 'HTMLDivElement', + dl: 'HTMLDListElement', + dt: 'HTMLElement', + em: 'HTMLElement', + embed: 'HTMLEmbedElement', + fieldset: 'HTMLFieldSetElement', + figcaption: 'HTMLElement', + figure: 'HTMLElement', + font: 'HTMLFontElement', + footer: 'HTMLElement', + form: 'HTMLFormElement', + frame: 'HTMLFrameElement', + frameset: 'HTMLFrameSetElement', + h1: 'HTMLHeadingElement', + h2: 'HTMLHeadingElement', + h3: 'HTMLHeadingElement', + h4: 'HTMLHeadingElement', + h5: 'HTMLHeadingElement', + h6: 'HTMLHeadingElement', + head: 'HTMLHeadElement', + header: 'HTMLElement', + hgroup: 'HTMLElement', + hr: 'HTMLHRElement', + html: 'HTMLHtmlElement', + i: 'HTMLElement', + iframe: 'HTMLIFrameElement', + img: 'HTMLImageElement', + index: 'HTMLIsIndexElement', + input: 'HTMLInputElement', + ins: 'HTMLModElement', + kbd: 'HTMLElement', + keygen: 'HTMLKeygenElement', + label: 'HTMLLabelElement', + legend: 'HTMLLegendElement', + li: 'HTMLLIElement', + link: 'HTMLLinkElement', + main: 'HTMLElement', + map: 'HTMLMapElement', + mark: 'HTMLElement', + marquee: 'HTMLMarqueeElement', + menu: 'HTMLMenuElement', + meta: 'HTMLMetaElement', + meter: 'HTMLMeterElement', + multicol: 'HTMLElement', + nav: 'HTMLElement', + nextid: 'HTMLNextIdElement', + noscript: 'HTMLElement', + object: 'HTMLObjectElement', + ol: 'HTMLOListElement', + optgroup: 'HTMLOptGroupElement', + option: 'HTMLOptionElement', + output: 'HTMLOutputElement', + p: 'HTMLParagraphElement', + param: 'HTMLParamElement', + picture: 'HTMLPictureElement', + pre: 'HTMLPreElement', + progress: 'HTMLProgressElement', + q: 'HTMLQuoteElement', + rb: 'HTMLElement', + rp: 'HTMLElement', + rt: 'HTMLElement', + ruby: 'HTMLElement', + s: 'HTMLElement', + samp: 'HTMLElement', + script: 'HTMLScriptElement', + section: 'HTMLElement', + select: 'HTMLSelectElement', + shadow: 'HTMLShadowElement', + slot: 'HTMLSlotElement', + small: 'HTMLElement', + source: 'HTMLSourceElement', + spacer: 'HTMLElement', + span: 'HTMLSpanElement', + strong: 'HTMLElement', + style: 'HTMLStyleElement', + summary: 'HTMLElement', + table: 'HTMLTableElement', + tbody: 'HTMLTableSectionElement', + td: 'HTMLTableCellElement', + template: 'HTMLTemplateElement', + textarea: 'HTMLTextAreaElement', + tfoot: 'HTMLTableSectionElement', + th: 'HTMLTableCellElement', + thead: 'HTMLTableSectionElement', + time: 'HTMLTimeElement', + title: 'HTMLTitleElement', + tr: 'HTMLTableRowElement', + track: 'HTMLTrackElement', + u: 'HTMLElement', + ul: 'HTMLUListElement', + var: 'HTMLElement', + video: 'HTMLVideoElement', + wbr: 'HTMLElement' +} From 69cd5e0f7a14eb376b299d065a12c16fcf753582 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Wed, 24 Mar 2021 18:30:15 +0000 Subject: [PATCH 2/6] feat: add extends-correct-class rule --- README.md | 1 + docs/rules/extends-correct-class.md | 41 +++++++++++++++++++ lib/rules.js | 3 +- lib/rules/extends-correct-class.js | 62 +++++++++++++++++++++++++++++ test/extends-correct-class.js | 59 +++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 docs/rules/extends-correct-class.md create mode 100644 lib/rules/extends-correct-class.js create mode 100644 test/extends-correct-class.js diff --git a/README.md b/README.md index 5cb02e1..07e6add 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) + - [Extends Correct Class](./docs/rules/extends-correct-class.md) diff --git a/docs/rules/extends-correct-class.md b/docs/rules/extends-correct-class.md new file mode 100644 index 0000000..d6eebbe --- /dev/null +++ b/docs/rules/extends-correct-class.md @@ -0,0 +1,41 @@ +# Extends Correct Class + +There are two distinct types of Custom Elements that can be defined: + + - An **autonomous custom element**, which is defined with no `extends` option. + - A **customized built-in element**, which is defined with an `extends` option. + +The [specification defines the requirements of the superclass for each of these types](https://html.spec.whatwg.org/multipage/dom.html#html-element-constructors). Autonomous custom elements _must_ extends the base `HTMLElement` class. Customized built in elements _must_ extend the base constructor of the element they wish to extend, for example an element that is defined as `extends: "p"` must itself `extends HTMLParagraphElement`. Trying to extend from another class will silently fail, and the browser will not upgrade the element to the desired class. + +## Rule Details + +This rule enforces that any call to `customElements.define` must be given the correct superclass. If the `extends` option is passed, then the given classes superclass must match the named element. If the `extends` option is not passed, then the given classes superclass must be `HTMLElement`. + +👎 Examples of **incorrect** code for this rule: + +```js +customElements.define('foo-bar', class extends HTMLParagraphElement) +// ^ `foo-bar` extends HTMLParagraphElement but define call did not extend `p` +``` +```js +customElements.define('foo-bar', class extends HTMLElement, { extends: 'p' }) +// ^ `foo-bar` extends `p` but it extends HTMLElement +``` + +👍 Examples of **correct** code for this rule: + +```js +customElements.define('foo-bar', class extends HTMLElement) +``` + +```js +customElements.define('foo-bar', class extends HTMLParagraphElement, { extends: 'p' }) +``` + +## When Not To Use It + + + +## Version + +This rule was introduced in 0.0.1 diff --git a/lib/rules.js b/lib/rules.js index c70cc4f..67c7760 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'), + 'extends-correct-class': require('./rules/extends-correct-class') } diff --git a/lib/rules/extends-correct-class.js b/lib/rules/extends-correct-class.js new file mode 100644 index 0000000..f98f1a4 --- /dev/null +++ b/lib/rules/extends-correct-class.js @@ -0,0 +1,62 @@ +const s = require('../custom-selectors') +const ClassRefTracker = require('../class-ref-tracker') +const {builtInTagMap} = require('../tag-names') + +function getExtendsOption(node) { + if (!node) return null + if (node.type !== 'ObjectExpression') return null + const extendsOption = node.properties.find(p => p.key.name === 'extends') + if (!extendsOption) return null + const value = extendsOption.value + if (value.type === 'Literal') return value.value + if (value.type === 'TemplateLiteral' && value.expressions.length === 0) { + return value.quasis.map(q => q.value.raw).join('') + } + return null +} + +module.exports = { + meta: { + type: 'problem', + docs: {description: '', url: require('../url')(module)} + }, + schema: [], + create(context) { + const classes = new ClassRefTracker(context) + return { + [s.HTMLElementClass](node) { + classes.add(node) + }, + [s.customElements.define](node) { + const classRef = classes.get(node.arguments[1]) + if (!classRef) return + const extendsTag = getExtendsOption(node.arguments[2]) + const desiredSuperName = builtInTagMap[extendsTag] || 'HTMLElement' + if (!classRef.superClass) { + return context.report(node.arguments[1], `Custom Element must extend ${desiredSuperName}`) + } + const superName = classRef.superClass.name + if (superName !== desiredSuperName) { + const expectedTag = Object.entries(builtInTagMap).find(e => e[1] === superName)?.[0] || null + if (extendsTag && !expectedTag) { + context.report(node, `${superName} !== ${desiredSuperName}`) + } else if (!extendsTag && expectedTag) { + context.report( + node, + `Custom Element must extend ${desiredSuperName}, or pass {extends:'${expectedTag}'} as a third argument to define` + ) + } else if (extendsTag !== expectedTag) { + context.report( + node, + `Custom Element extends ${superName} but the definition includes {extends:'${extendsTag}'}. ` + + `Either the Custom Element must extend from ${desiredSuperName}, ` + + `or the definition must include {extends:'${expectedTag}'}.` + ) + } else { + context.report(node, `Custom Element must extend ${desiredSuperName} not ${superName}`) + } + } + } + } + } +} diff --git a/test/extends-correct-class.js b/test/extends-correct-class.js new file mode 100644 index 0000000..13bac86 --- /dev/null +++ b/test/extends-correct-class.js @@ -0,0 +1,59 @@ +const rule = require('../lib/rules/extends-correct-class') +const RuleTester = require('eslint').RuleTester + +const ruleTester = new RuleTester({env: {es2020: true}}) + +ruleTester.run('extends-correct-class', rule, { + valid: [ + {code: 'customElements.define("foo-bar", class extends HTMLElement {})'}, + {code: 'customElements.define("foo-bar", class extends HTMLDivElement {}, { extends: "div" })'}, + {code: 'customElements.define("foo-bar", class extends HTMLOListElement {}, { extends: `ol` })'} + ], + invalid: [ + { + code: 'customElements.define("foo-bar", class {})', + errors: [ + { + message: 'Custom Element must extend HTMLElement', + type: 'ClassExpression' + } + ] + }, + { + code: 'customElements.define("foo-bar", class {}, { extends: "p" })', + errors: [ + { + message: 'Custom Element must extend HTMLParagraphElement', + type: 'ClassExpression' + } + ] + }, + { + code: 'customElements.define("foo-bar", class extends Map {})', + errors: [ + { + message: 'Custom Element must extend HTMLElement not Map', + type: 'CallExpression' + } + ] + }, + { + code: 'customElements.define("foo-bar", class extends HTMLParagraphElement {})', + errors: [ + { + message: "Custom Element must extend HTMLElement, or pass {extends:'p'} as a third argument to define", + type: 'CallExpression' + } + ] + }, + { + code: 'customElements.define("foo-bar", class extends HTMLDivElement {}, { extends: "p" })', + errors: [ + { + message: "Custom Element extends HTMLDivElement but the definition includes {extends:'p'}. Either the Custom Element must extend from HTMLParagraphElement, or the definition must include {extends:'div'}.", + type: 'CallExpression' + } + ] + } + ] +}) From 67599565b9ac133881bd478a525fd9fa98584386 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Wed, 24 Mar 2021 18:30:35 +0000 Subject: [PATCH 3/6] feat: add minimal config, errors only on `problem`s --- lib/configs/minimal.js | 14 ++++++++++++++ lib/index.js | 1 + 2 files changed, 15 insertions(+) create mode 100644 lib/configs/minimal.js diff --git a/lib/configs/minimal.js b/lib/configs/minimal.js new file mode 100644 index 0000000..758d1a6 --- /dev/null +++ b/lib/configs/minimal.js @@ -0,0 +1,14 @@ +const rules = require('../rules') +module.exports = { + parserOptions: { + sourceType: 'module' + }, + env: { + es2021: true + }, + rules: Object.fromEntries( + Object.keys(rules) + .filter(r => rules[r].meta.type === 'problem') + .map(r => [`custom-elements/${r}`, 'error']) + ) +} diff --git a/lib/index.js b/lib/index.js index 0302d28..2ed6507 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,7 @@ module.exports = { rules: require('./rules'), configs: { + minimal: require('./configs/minimal'), recommended: require('./configs/recommended') } } From 771f46171086d08ecfb3c9b4c2c75df0df74ab1a Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Fri, 26 Mar 2021 17:36:45 +0000 Subject: [PATCH 4/6] docs(extends-correct-class): add note on when not to use it. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kristján Oddsson --- docs/rules/extends-correct-class.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/extends-correct-class.md b/docs/rules/extends-correct-class.md index d6eebbe..a971d9a 100644 --- a/docs/rules/extends-correct-class.md +++ b/docs/rules/extends-correct-class.md @@ -34,7 +34,7 @@ customElements.define('foo-bar', class extends HTMLParagraphElement, { extends: ## When Not To Use It - +If you are comfortable with silent failures when extending types don't match. ## Version From 49a6205947851605d41c175321140d7419bb370f Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Fri, 26 Mar 2021 17:41:55 +0000 Subject: [PATCH 5/6] docs(readme): fix merge errors --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 04e7b90..0e54177 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,6 @@ 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) - [Extends Correct Class](./docs/rules/extends-correct-class.md) @@ -32,4 +31,4 @@ JSON ESLint config example: - [No Customized Built in Elements](./docs/rules/no-customized-built-in-elements.md) - [No DOM Traversal in Connectedcallback](./docs/rules/no-dom-traversal-in-connectedcallback.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) From cc69d98b44656b45c9d17035fd37ac06f528999f Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Fri, 26 Mar 2021 17:43:26 +0000 Subject: [PATCH 6/6] style: `npm run lint -- --fix` --- lib/tag-names.js | 2 +- test/extends-correct-class.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/tag-names.js b/lib/tag-names.js index 6eccfd0..8086bc6 100644 --- a/lib/tag-names.js +++ b/lib/tag-names.js @@ -281,4 +281,4 @@ const PCENChar = [ module.exports.PCENChar = PCENChar const customElementNameRegExp = new RegExp(`^[a-z]${PCENChar}*-${PCENChar}*$`) -module.exports.validTagName = name => customElementNameRegExp.test(name) \ No newline at end of file +module.exports.validTagName = name => customElementNameRegExp.test(name) diff --git a/test/extends-correct-class.js b/test/extends-correct-class.js index 13bac86..970f878 100644 --- a/test/extends-correct-class.js +++ b/test/extends-correct-class.js @@ -50,7 +50,8 @@ ruleTester.run('extends-correct-class', rule, { code: 'customElements.define("foo-bar", class extends HTMLDivElement {}, { extends: "p" })', errors: [ { - message: "Custom Element extends HTMLDivElement but the definition includes {extends:'p'}. Either the Custom Element must extend from HTMLParagraphElement, or the definition must include {extends:'div'}.", + message: + "Custom Element extends HTMLDivElement but the definition includes {extends:'p'}. Either the Custom Element must extend from HTMLParagraphElement, or the definition must include {extends:'div'}.", type: 'CallExpression' } ]