[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Prevent declaring unused methods and properties of component class
|
---|
| 3 | * @author Paweł Nowak, Berton Zhu
|
---|
| 4 | */
|
---|
| 5 |
|
---|
| 6 | 'use strict';
|
---|
| 7 |
|
---|
| 8 | const docsUrl = require('../util/docsUrl');
|
---|
| 9 | const componentUtil = require('../util/componentUtil');
|
---|
| 10 | const report = require('../util/report');
|
---|
| 11 |
|
---|
| 12 | // ------------------------------------------------------------------------------
|
---|
| 13 | // Rule Definition
|
---|
| 14 | // ------------------------------------------------------------------------------
|
---|
| 15 |
|
---|
| 16 | const LIFECYCLE_METHODS = new Set([
|
---|
| 17 | 'constructor',
|
---|
| 18 | 'componentDidCatch',
|
---|
| 19 | 'componentDidMount',
|
---|
| 20 | 'componentDidUpdate',
|
---|
| 21 | 'componentWillMount',
|
---|
| 22 | 'componentWillReceiveProps',
|
---|
| 23 | 'componentWillUnmount',
|
---|
| 24 | 'componentWillUpdate',
|
---|
| 25 | 'getChildContext',
|
---|
| 26 | 'getSnapshotBeforeUpdate',
|
---|
| 27 | 'render',
|
---|
| 28 | 'shouldComponentUpdate',
|
---|
| 29 | 'UNSAFE_componentWillMount',
|
---|
| 30 | 'UNSAFE_componentWillReceiveProps',
|
---|
| 31 | 'UNSAFE_componentWillUpdate',
|
---|
| 32 | ]);
|
---|
| 33 |
|
---|
| 34 | const ES6_LIFECYCLE = new Set([
|
---|
| 35 | 'state',
|
---|
| 36 | ]);
|
---|
| 37 |
|
---|
| 38 | const ES5_LIFECYCLE = new Set([
|
---|
| 39 | 'getInitialState',
|
---|
| 40 | 'getDefaultProps',
|
---|
| 41 | 'mixins',
|
---|
| 42 | ]);
|
---|
| 43 |
|
---|
| 44 | function isKeyLiteralLike(node, property) {
|
---|
| 45 | return property.type === 'Literal'
|
---|
| 46 | || (property.type === 'TemplateLiteral' && property.expressions.length === 0)
|
---|
| 47 | || (node.computed === false && property.type === 'Identifier');
|
---|
| 48 | }
|
---|
| 49 |
|
---|
| 50 | // Descend through all wrapping TypeCastExpressions and return the expression
|
---|
| 51 | // that was cast.
|
---|
| 52 | function uncast(node) {
|
---|
| 53 | while (node.type === 'TypeCastExpression') {
|
---|
| 54 | node = node.expression;
|
---|
| 55 | }
|
---|
| 56 | return node;
|
---|
| 57 | }
|
---|
| 58 |
|
---|
| 59 | // Return the name of an identifier or the string value of a literal. Useful
|
---|
| 60 | // anywhere that a literal may be used as a key (e.g., member expressions,
|
---|
| 61 | // method definitions, ObjectExpression property keys).
|
---|
| 62 | function getName(node) {
|
---|
| 63 | node = uncast(node);
|
---|
| 64 | const type = node.type;
|
---|
| 65 |
|
---|
| 66 | if (type === 'Identifier') {
|
---|
| 67 | return node.name;
|
---|
| 68 | }
|
---|
| 69 | if (type === 'Literal') {
|
---|
| 70 | return String(node.value);
|
---|
| 71 | }
|
---|
| 72 | if (type === 'TemplateLiteral' && node.expressions.length === 0) {
|
---|
| 73 | return node.quasis[0].value.raw;
|
---|
| 74 | }
|
---|
| 75 | return null;
|
---|
| 76 | }
|
---|
| 77 |
|
---|
| 78 | function isThisExpression(node) {
|
---|
| 79 | return uncast(node).type === 'ThisExpression';
|
---|
| 80 | }
|
---|
| 81 |
|
---|
| 82 | function getInitialClassInfo(node, isClass) {
|
---|
| 83 | return {
|
---|
| 84 | classNode: node,
|
---|
| 85 | isClass,
|
---|
| 86 | // Set of nodes where properties were defined.
|
---|
| 87 | properties: new Set(),
|
---|
| 88 |
|
---|
| 89 | // Set of names of properties that we've seen used.
|
---|
| 90 | usedProperties: new Set(),
|
---|
| 91 |
|
---|
| 92 | inStatic: false,
|
---|
| 93 | };
|
---|
| 94 | }
|
---|
| 95 |
|
---|
| 96 | const messages = {
|
---|
| 97 | unused: 'Unused method or property "{{name}}"',
|
---|
| 98 | unusedWithClass: 'Unused method or property "{{name}}" of class "{{className}}"',
|
---|
| 99 | };
|
---|
| 100 |
|
---|
| 101 | module.exports = {
|
---|
| 102 | meta: {
|
---|
| 103 | docs: {
|
---|
| 104 | description: 'Disallow declaring unused methods of component class',
|
---|
| 105 | category: 'Best Practices',
|
---|
| 106 | recommended: false,
|
---|
| 107 | url: docsUrl('no-unused-class-component-methods'),
|
---|
| 108 | },
|
---|
| 109 | messages,
|
---|
| 110 | schema: [],
|
---|
| 111 | },
|
---|
| 112 |
|
---|
| 113 | create: ((context) => {
|
---|
| 114 | let classInfo = null;
|
---|
| 115 |
|
---|
| 116 | // Takes an ObjectExpression node and adds all named Property nodes to the
|
---|
| 117 | // current set of properties.
|
---|
| 118 | function addProperty(node) {
|
---|
| 119 | classInfo.properties.add(node);
|
---|
| 120 | }
|
---|
| 121 |
|
---|
| 122 | // Adds the name of the given node as a used property if the node is an
|
---|
| 123 | // Identifier or a Literal. Other node types are ignored.
|
---|
| 124 | function addUsedProperty(node) {
|
---|
| 125 | const name = getName(node);
|
---|
| 126 | if (name) {
|
---|
| 127 | classInfo.usedProperties.add(name);
|
---|
| 128 | }
|
---|
| 129 | }
|
---|
| 130 |
|
---|
| 131 | function reportUnusedProperties() {
|
---|
| 132 | // Report all unused properties.
|
---|
| 133 | for (const node of classInfo.properties) { // eslint-disable-line no-restricted-syntax
|
---|
| 134 | const name = getName(node);
|
---|
| 135 | if (
|
---|
| 136 | !classInfo.usedProperties.has(name)
|
---|
| 137 | && !LIFECYCLE_METHODS.has(name)
|
---|
| 138 | && (classInfo.isClass ? !ES6_LIFECYCLE.has(name) : !ES5_LIFECYCLE.has(name))
|
---|
| 139 | ) {
|
---|
| 140 | const className = (classInfo.classNode.id && classInfo.classNode.id.name) || '';
|
---|
| 141 |
|
---|
| 142 | const messageID = className ? 'unusedWithClass' : 'unused';
|
---|
| 143 | report(
|
---|
| 144 | context,
|
---|
| 145 | messages[messageID],
|
---|
| 146 | messageID,
|
---|
| 147 | {
|
---|
| 148 | node,
|
---|
| 149 | data: {
|
---|
| 150 | name,
|
---|
| 151 | className,
|
---|
| 152 | },
|
---|
| 153 | }
|
---|
| 154 | );
|
---|
| 155 | }
|
---|
| 156 | }
|
---|
| 157 | }
|
---|
| 158 |
|
---|
| 159 | function exitMethod() {
|
---|
| 160 | if (!classInfo || !classInfo.inStatic) {
|
---|
| 161 | return;
|
---|
| 162 | }
|
---|
| 163 |
|
---|
| 164 | classInfo.inStatic = false;
|
---|
| 165 | }
|
---|
| 166 |
|
---|
| 167 | return {
|
---|
| 168 | ClassDeclaration(node) {
|
---|
| 169 | if (componentUtil.isES6Component(node, context)) {
|
---|
| 170 | classInfo = getInitialClassInfo(node, true);
|
---|
| 171 | }
|
---|
| 172 | },
|
---|
| 173 |
|
---|
| 174 | ObjectExpression(node) {
|
---|
| 175 | if (componentUtil.isES5Component(node, context)) {
|
---|
| 176 | classInfo = getInitialClassInfo(node, false);
|
---|
| 177 | }
|
---|
| 178 | },
|
---|
| 179 |
|
---|
| 180 | 'ClassDeclaration:exit'() {
|
---|
| 181 | if (!classInfo) {
|
---|
| 182 | return;
|
---|
| 183 | }
|
---|
| 184 | reportUnusedProperties();
|
---|
| 185 | classInfo = null;
|
---|
| 186 | },
|
---|
| 187 |
|
---|
| 188 | 'ObjectExpression:exit'(node) {
|
---|
| 189 | if (!classInfo || classInfo.classNode !== node) {
|
---|
| 190 | return;
|
---|
| 191 | }
|
---|
| 192 | reportUnusedProperties();
|
---|
| 193 | classInfo = null;
|
---|
| 194 | },
|
---|
| 195 |
|
---|
| 196 | Property(node) {
|
---|
| 197 | if (!classInfo || classInfo.classNode !== node.parent) {
|
---|
| 198 | return;
|
---|
| 199 | }
|
---|
| 200 |
|
---|
| 201 | if (isKeyLiteralLike(node, node.key)) {
|
---|
| 202 | addProperty(node.key);
|
---|
| 203 | }
|
---|
| 204 | },
|
---|
| 205 |
|
---|
| 206 | 'ClassProperty, MethodDefinition, PropertyDefinition'(node) {
|
---|
| 207 | if (!classInfo) {
|
---|
| 208 | return;
|
---|
| 209 | }
|
---|
| 210 |
|
---|
| 211 | if (node.static) {
|
---|
| 212 | classInfo.inStatic = true;
|
---|
| 213 | return;
|
---|
| 214 | }
|
---|
| 215 |
|
---|
| 216 | if (isKeyLiteralLike(node, node.key)) {
|
---|
| 217 | addProperty(node.key);
|
---|
| 218 | }
|
---|
| 219 | },
|
---|
| 220 |
|
---|
| 221 | 'ClassProperty:exit': exitMethod,
|
---|
| 222 | 'MethodDefinition:exit': exitMethod,
|
---|
| 223 | 'PropertyDefinition:exit': exitMethod,
|
---|
| 224 |
|
---|
| 225 | MemberExpression(node) {
|
---|
| 226 | if (!classInfo || classInfo.inStatic) {
|
---|
| 227 | return;
|
---|
| 228 | }
|
---|
| 229 |
|
---|
| 230 | if (isThisExpression(node.object) && isKeyLiteralLike(node, node.property)) {
|
---|
| 231 | if (node.parent.type === 'AssignmentExpression' && node.parent.left === node) {
|
---|
| 232 | // detect `this.property = xxx`
|
---|
| 233 | addProperty(node.property);
|
---|
| 234 | } else {
|
---|
| 235 | // detect `this.property()`, `x = this.property`, etc.
|
---|
| 236 | addUsedProperty(node.property);
|
---|
| 237 | }
|
---|
| 238 | }
|
---|
| 239 | },
|
---|
| 240 |
|
---|
| 241 | VariableDeclarator(node) {
|
---|
| 242 | if (!classInfo || classInfo.inStatic) {
|
---|
| 243 | return;
|
---|
| 244 | }
|
---|
| 245 |
|
---|
| 246 | // detect `{ foo, bar: baz } = this`
|
---|
| 247 | if (node.init && isThisExpression(node.init) && node.id.type === 'ObjectPattern') {
|
---|
| 248 | node.id.properties
|
---|
| 249 | .filter((prop) => prop.type === 'Property' && isKeyLiteralLike(prop, prop.key))
|
---|
| 250 | .forEach((prop) => {
|
---|
| 251 | addUsedProperty(prop.key);
|
---|
| 252 | });
|
---|
| 253 | }
|
---|
| 254 | },
|
---|
| 255 | };
|
---|
| 256 | }),
|
---|
| 257 | };
|
---|