-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat (define-tag-after-class-definition): new rule
- Loading branch information
Showing
4 changed files
with
245 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
/** | ||
* @fileoverview Enforces that the `define(...)` call happens after the | ||
* associated class has been defined | ||
* @author James Garbutt <https://github.com/43081j> | ||
*/ | ||
|
||
import {Rule} from 'eslint'; | ||
import * as ESTree from 'estree'; | ||
import {isCustomElement, isDefineCall} from '../util'; | ||
|
||
//------------------------------------------------------------------------------ | ||
// Rule Definition | ||
//------------------------------------------------------------------------------ | ||
|
||
const rule: Rule.RuleModule = { | ||
meta: { | ||
docs: { | ||
description: | ||
'Enforces that the `define(...)` call happens after' + | ||
' the associated class has been defined', | ||
url: 'https://github.com/43081j/eslint-plugin-wc/blob/master/docs/rules/define-tag-after-class-definition.md' | ||
}, | ||
messages: { | ||
unregistered: | ||
'Custom element class has not been registered with ' + | ||
' a `customElements.define` call', | ||
noExpressions: | ||
'Custom element classes should not be declared inline. ' + | ||
'They should be exported as concrete class declarations.' | ||
} | ||
}, | ||
|
||
create(context): Rule.RuleListener { | ||
const seenClasses = new Set<ESTree.Node>(); | ||
const source = context.getSourceCode(); | ||
|
||
//---------------------------------------------------------------------- | ||
// Helpers | ||
//---------------------------------------------------------------------- | ||
|
||
//---------------------------------------------------------------------- | ||
// Public | ||
//---------------------------------------------------------------------- | ||
|
||
return { | ||
'ClassDeclaration,ClassExpression': (node: ESTree.Class): void => { | ||
if ( | ||
isCustomElement(context, node, source.getJSDocComment(node)) && | ||
node.id?.type === 'Identifier' | ||
) { | ||
seenClasses.add(node); | ||
} | ||
}, | ||
CallExpression: (node: ESTree.CallExpression): void => { | ||
const tagClass = node.arguments[1]; | ||
|
||
if (isDefineCall(node)) { | ||
if (tagClass.type === 'Identifier') { | ||
const ref = context | ||
.getScope() | ||
.references.find((r) => r.identifier.name === tagClass.name); | ||
|
||
if (ref?.resolved && ref.resolved.defs.length === 1) { | ||
seenClasses.delete(ref.resolved.defs[0].node); | ||
} | ||
} else if (tagClass.type === 'ClassExpression') { | ||
seenClasses.delete(tagClass); | ||
context.report({ | ||
node: tagClass, | ||
messageId: 'noExpressions' | ||
}); | ||
} | ||
} | ||
}, | ||
'Program:exit': (): void => { | ||
for (const node of seenClasses) { | ||
context.report({ | ||
node, | ||
messageId: 'unregistered' | ||
}); | ||
} | ||
|
||
seenClasses.clear(); | ||
} | ||
}; | ||
} | ||
}; | ||
|
||
export default rule; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
134 changes: 134 additions & 0 deletions
134
src/test/rules/define-tag-after-class-definition_test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
/** | ||
* @fileoverview Enforces that the `define(...)` call happens after the | ||
* associated class has been defined | ||
* @author James Garbutt <https://github.com/43081j> | ||
*/ | ||
|
||
//------------------------------------------------------------------------------ | ||
// Requirements | ||
//------------------------------------------------------------------------------ | ||
|
||
import rule from '../../rules/define-tag-after-class-definition'; | ||
import {RuleTester} from 'eslint'; | ||
|
||
//------------------------------------------------------------------------------ | ||
// Tests | ||
//------------------------------------------------------------------------------ | ||
|
||
const ruleTester = new RuleTester({ | ||
parserOptions: { | ||
sourceType: 'module', | ||
ecmaVersion: 2015 | ||
} | ||
}); | ||
|
||
ruleTester.run('define-tag-after-class-definition', rule, { | ||
valid: [ | ||
'class Foo {}', | ||
`class Foo extends HTMLElement {} | ||
customElements.define('x-foo', Foo);`, | ||
`class Foo extends HTMLElement {} | ||
const x = 303; | ||
customElements.define('x-foo', Foo);`, | ||
`class Foo extends HTMLElement {} | ||
window.customElements.define('x-foo', Foo);`, | ||
`/** @customElement x-foo */ | ||
class Foo extends Bar {} | ||
customElements.define('x-foo', Foo);`, | ||
`customElements.define('x-foo', Foo);`, | ||
`const Foo = doSomeBlackBoxThing(); | ||
customElements.define('x-foo', Foo);`, | ||
{ | ||
code: `class Foo extends Bar {} | ||
customElements.define('x-foo', Foo);`, | ||
settings: { | ||
wc: { | ||
elementBaseClasses: ['Bar'] | ||
} | ||
} | ||
} | ||
], | ||
|
||
invalid: [ | ||
{ | ||
code: 'class Foo extends HTMLElement {}', | ||
errors: [ | ||
{ | ||
messageId: 'unregistered', | ||
line: 1, | ||
column: 1 | ||
} | ||
] | ||
}, | ||
{ | ||
code: `class Foo extends HTMLElement {} | ||
customElements.define('x-foo', someThingElse);`, | ||
errors: [ | ||
{ | ||
messageId: 'unregistered', | ||
line: 1, | ||
column: 1 | ||
} | ||
] | ||
}, | ||
{ | ||
code: `class Foo extends HTMLElement {} | ||
customElements.define('x-foo', someDynamicThing());`, | ||
errors: [ | ||
{ | ||
messageId: 'unregistered', | ||
line: 1, | ||
column: 1 | ||
} | ||
] | ||
}, | ||
{ | ||
code: `class Foo extends HTMLElement {} | ||
someOtherNonsense.define('x-foo', Foo);`, | ||
errors: [ | ||
{ | ||
messageId: 'unregistered', | ||
line: 1, | ||
column: 1 | ||
} | ||
] | ||
}, | ||
{ | ||
code: `customElements.define('x-foo', class extends HTMLElement { | ||
});`, | ||
errors: [ | ||
{ | ||
messageId: 'noExpressions', | ||
line: 1, | ||
column: 32 | ||
} | ||
] | ||
}, | ||
{ | ||
code: 'class Foo extends Bar {}', | ||
settings: { | ||
wc: { | ||
elementBaseClasses: ['Bar'] | ||
} | ||
}, | ||
errors: [ | ||
{ | ||
messageId: 'unregistered', | ||
line: 1, | ||
column: 1 | ||
} | ||
] | ||
}, | ||
{ | ||
code: `/** @customElement x-foo */ | ||
class Foo extends Bar {}`, | ||
errors: [ | ||
{ | ||
messageId: 'unregistered', | ||
line: 2, | ||
column: 7 | ||
} | ||
] | ||
} | ||
] | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters