-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: no-attributes-during-construction (#61)
- Loading branch information
Showing
7 changed files
with
483 additions
and
18 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
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,88 @@ | ||
# Disallow setting attributes during construction (no-attributes-during-construction) | ||
|
||
The `LightningElement` base class extended by LWC component classes defines several properties that, when set, renders | ||
attributes on its host element. This behavior mimics the native browser behavior. | ||
|
||
By [specification](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance), constructors | ||
must not cause the host element to gain attributes. This rule prevents set operations in the constructor method that | ||
violate this restriction. | ||
|
||
## Caveats | ||
|
||
This rule only knows about `LightningElement` properties that implement this behavior (e.g., `hidden`, `id`, `role`, | ||
`tabIndex`, `title`, etc) and will not detect custom implementations that may set attributes during construction time: | ||
|
||
```js | ||
import { LightningElement } from 'lwc'; | ||
|
||
export default class Test extends LightningElement { | ||
set foo(val) { | ||
this.setAttribute('foo', val); | ||
} | ||
|
||
constructor() { | ||
this.foo = 'this causes the element to gain the foo attribute during construction'; | ||
} | ||
} | ||
``` | ||
|
||
This rule will not detect violations on component classes that do not directly inherit from `LightningElement`: | ||
|
||
```js | ||
import { LightningElement } from 'lwc'; | ||
|
||
class Base extends LightningElement {} | ||
|
||
export default class Test extends Base { | ||
constructor() { | ||
this.title = 'this causes the element to gain the foo attribute during construction'; | ||
} | ||
} | ||
``` | ||
|
||
## Examples | ||
|
||
### Invalid | ||
|
||
The following example is setting the `title` property which the `LightningElement` base class provides by default and | ||
this renders the `title` attribute on the host element. | ||
|
||
```js | ||
import { LightningElement } from 'lwc'; | ||
|
||
export default class Test extends LightningElement { | ||
constructor() { | ||
this.title = 'this causes the element to gain the title attribute during construction'; | ||
} | ||
} | ||
``` | ||
|
||
### Valid | ||
|
||
The following example does not set the value of `title` inside the constructor. | ||
|
||
```js | ||
import { LightningElement } from 'lwc'; | ||
|
||
export default class Test extends LightningElement { | ||
connectedCallback() { | ||
this.title = 'this causes the element to gain the title attribute upon connection'; | ||
} | ||
} | ||
``` | ||
|
||
The following example overrides the `title` property which the `LightningElement` base class provides by default, with a | ||
`title` property that, when set, does not render an attribute on the host element. | ||
|
||
```js | ||
import { LightningElement } from 'lwc'; | ||
|
||
export default class Test extends LightningElement { | ||
title = 'this custom property overrides the one in LightningElement'; | ||
|
||
constructor() { | ||
this.title = | ||
'this does not cause the element to gain the title attribute during construction'; | ||
} | ||
} | ||
``` |
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
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,152 @@ | ||
/* | ||
* Copyright (c) 2018, salesforce.com, inc. | ||
* All rights reserved. | ||
* SPDX-License-Identifier: MIT | ||
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT | ||
*/ | ||
'use strict'; | ||
|
||
const { isComponent } = require('../util/component'); | ||
const { docUrl } = require('../util/doc-url'); | ||
|
||
// https://github.com/salesforce/lwc/blob/6e6ad1011887d842e3f03546c6a81e9cee057611/packages/%40lwc/shared/src/aria.ts#L10-L67 | ||
const ARIA_PROPERTY_NAMES = [ | ||
'ariaActiveDescendant', | ||
'ariaAtomic', | ||
'ariaAutoComplete', | ||
'ariaBusy', | ||
'ariaChecked', | ||
'ariaColCount', | ||
'ariaColIndex', | ||
'ariaColSpan', | ||
'ariaControls', | ||
'ariaCurrent', | ||
'ariaDescribedBy', | ||
'ariaDetails', | ||
'ariaDisabled', | ||
'ariaErrorMessage', | ||
'ariaExpanded', | ||
'ariaFlowTo', | ||
'ariaHasPopup', | ||
'ariaHidden', | ||
'ariaInvalid', | ||
'ariaKeyShortcuts', | ||
'ariaLabel', | ||
'ariaLabelledBy', | ||
'ariaLevel', | ||
'ariaLive', | ||
'ariaModal', | ||
'ariaMultiLine', | ||
'ariaMultiSelectable', | ||
'ariaOrientation', | ||
'ariaOwns', | ||
'ariaPlaceholder', | ||
'ariaPosInSet', | ||
'ariaPressed', | ||
'ariaReadOnly', | ||
'ariaRelevant', | ||
'ariaRequired', | ||
'ariaRoleDescription', | ||
'ariaRowCount', | ||
'ariaRowIndex', | ||
'ariaRowSpan', | ||
'ariaSelected', | ||
'ariaSetSize', | ||
'ariaSort', | ||
'ariaValueMax', | ||
'ariaValueMin', | ||
'ariaValueNow', | ||
'ariaValueText', | ||
'role', | ||
]; | ||
|
||
// https://github.com/salesforce/lwc/blob/6e6ad1011887d842e3f03546c6a81e9cee057611/packages/%40lwc/engine-core/src/framework/attributes.ts#L9-L20 | ||
const LWC_DEFAULT_PROPERTY_NAMES = [ | ||
'accessKey', | ||
'dir', | ||
'draggable', | ||
'hidden', | ||
'id', | ||
'lang', | ||
'spellcheck', | ||
'tabIndex', | ||
'title', | ||
]; | ||
|
||
const PROPERTIES_THAT_REFLECT = new Set([...ARIA_PROPERTY_NAMES, ...LWC_DEFAULT_PROPERTY_NAMES]); | ||
|
||
function getAccessorMethodNames(bodyItems) { | ||
return bodyItems | ||
.filter( | ||
({ computed, kind, type }) => | ||
type === 'MethodDefinition' && (kind === 'get' || kind === 'set') && !computed, | ||
) | ||
.map(({ key }) => key.name); | ||
} | ||
|
||
function getClassPropertyNames(bodyItems) { | ||
return bodyItems | ||
.filter(({ computed, type }) => type === 'ClassProperty' && !computed) | ||
.map(({ key }) => key.name); | ||
} | ||
|
||
function getAssignmentExpressions(bodyItems) { | ||
return bodyItems | ||
.filter( | ||
({ expression, type }) => | ||
type === 'ExpressionStatement' && expression.type === 'AssignmentExpression', | ||
) | ||
.map(({ expression }) => expression); | ||
} | ||
|
||
module.exports = { | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: 'no attributes during construction', | ||
category: 'LWC', | ||
recommended: true, | ||
url: docUrl('no-attributes-during-construction'), | ||
}, | ||
schema: [], | ||
}, | ||
|
||
create(context) { | ||
return { | ||
'MethodDefinition[kind=constructor]': function (ctorMethod) { | ||
const classBody = ctorMethod.parent; | ||
|
||
// classBody.parent is ClassDeclaration or ClassExpression | ||
if (!isComponent(classBody.parent, context)) { | ||
return; | ||
} | ||
|
||
const blockStatement = ctorMethod.value.body; | ||
const assignmentExpressions = getAssignmentExpressions(blockStatement.body); | ||
|
||
if (assignmentExpressions.length !== 0) { | ||
const classPropertyNameSet = new Set(getClassPropertyNames(classBody.body)); | ||
const accessorMethodNameSet = new Set(getAccessorMethodNames(classBody.body)); | ||
|
||
for (const expression of assignmentExpressions) { | ||
const { left } = expression; | ||
const { computed, object, property } = left; | ||
if (object.type === 'ThisExpression' && !computed) { | ||
const { name } = property; | ||
if ( | ||
PROPERTIES_THAT_REFLECT.has(name) && | ||
!classPropertyNameSet.has(name) && | ||
!accessorMethodNameSet.has(name) | ||
) { | ||
context.report({ | ||
message: `Invariant violation: Setting "${name}" in the constructor results in a rendered attribute during construction. Change the name of this property, move this assignment to another lifecycle method, or override the default behavior by defining "${name}" as a property on the class.`, | ||
node: expression, | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
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
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
Oops, something went wrong.