Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Sep 18, 2023. It is now read-only.

Extends correct class #9

Merged
merged 7 commits into from
Mar 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ JSON ESLint config example:

- [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)
- [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)
- [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)
- [Valid Tag Name](./docs/rules/valid-tag-name.md)
41 changes: 41 additions & 0 deletions docs/rules/extends-correct-class.md
Original file line number Diff line number Diff line change
@@ -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

If you are comfortable with silent failures when extending types don't match.

## Version

This rule was introduced in 0.0.1
14 changes: 14 additions & 0 deletions lib/configs/minimal.js
Original file line number Diff line number Diff line change
@@ -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'])
)
}
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = {
rules: require('./rules'),
configs: {
minimal: require('./configs/minimal'),
recommended: require('./configs/recommended')
}
}
1 change: 1 addition & 0 deletions lib/rules.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = {
'define-tag-after-class-definition': require('./rules/define-tag-after-class-definition'),
'expose-class-on-global': require('./rules/expose-class-on-global'),
'extends-correct-class': require('./rules/extends-correct-class'),
'file-name-matches-element': require('./rules/file-name-matches-element'),
'no-constructor': require('./rules/no-constructor'),
'no-customized-built-in-elements': require('./rules/no-customized-built-in-elements'),
Expand Down
62 changes: 62 additions & 0 deletions lib/rules/extends-correct-class.js
Original file line number Diff line number Diff line change
@@ -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}`)
}
}
}
}
}
}
129 changes: 129 additions & 0 deletions lib/tag-names.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,132 @@
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'
}

module.exports.knownNamespaces = new Set([
'primer',
'polymer',
Expand Down
60 changes: 60 additions & 0 deletions test/extends-correct-class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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'
}
]
}
]
})