no-unused-properties.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. 'use strict';
  2. const getScopes = require('./utils/get-scopes.js');
  3. const MESSAGE_ID = 'no-unused-properties';
  4. const messages = {
  5. [MESSAGE_ID]: 'Property `{{name}}` is defined but never used.',
  6. };
  7. const getDeclaratorOrPropertyValue = declaratorOrProperty =>
  8. declaratorOrProperty.init
  9. || declaratorOrProperty.value;
  10. const isMemberExpressionCall = memberExpression =>
  11. memberExpression.parent.type === 'CallExpression'
  12. && memberExpression.parent.callee === memberExpression;
  13. const isMemberExpressionAssignment = memberExpression =>
  14. memberExpression.parent.type === 'AssignmentExpression';
  15. const isMemberExpressionComputedBeyondPrediction = memberExpression =>
  16. memberExpression.computed
  17. && memberExpression.property.type !== 'Literal';
  18. const specialProtoPropertyKey = {
  19. type: 'Identifier',
  20. name: '__proto__',
  21. };
  22. const propertyKeysEqual = (keyA, keyB) => {
  23. if (keyA.type === 'Identifier') {
  24. if (keyB.type === 'Identifier') {
  25. return keyA.name === keyB.name;
  26. }
  27. if (keyB.type === 'Literal') {
  28. return keyA.name === keyB.value;
  29. }
  30. }
  31. if (keyA.type === 'Literal') {
  32. if (keyB.type === 'Identifier') {
  33. return keyA.value === keyB.name;
  34. }
  35. if (keyB.type === 'Literal') {
  36. return keyA.value === keyB.value;
  37. }
  38. }
  39. return false;
  40. };
  41. const objectPatternMatchesObjectExprPropertyKey = (pattern, key) =>
  42. pattern.properties.some(property => {
  43. if (property.type === 'RestElement') {
  44. return true;
  45. }
  46. return propertyKeysEqual(property.key, key);
  47. });
  48. const isLeafDeclaratorOrProperty = declaratorOrProperty => {
  49. const value = getDeclaratorOrPropertyValue(declaratorOrProperty);
  50. if (!value) {
  51. return true;
  52. }
  53. if (value.type !== 'ObjectExpression') {
  54. return true;
  55. }
  56. return false;
  57. };
  58. const isUnusedVariable = variable => {
  59. const hasReadReference = variable.references.some(reference => reference.isRead());
  60. return !hasReadReference;
  61. };
  62. /** @param {import('eslint').Rule.RuleContext} context */
  63. const create = context => {
  64. const getPropertyDisplayName = property => {
  65. if (property.key.type === 'Identifier') {
  66. return property.key.name;
  67. }
  68. if (property.key.type === 'Literal') {
  69. return property.key.value;
  70. }
  71. return context.getSourceCode().getText(property.key);
  72. };
  73. const checkProperty = (property, references, path) => {
  74. if (references.length === 0) {
  75. context.report({
  76. node: property,
  77. messageId: MESSAGE_ID,
  78. data: {
  79. name: getPropertyDisplayName(property),
  80. },
  81. });
  82. return;
  83. }
  84. checkObject(property, references, path);
  85. };
  86. const checkProperties = (objectExpression, references, path = []) => {
  87. for (const property of objectExpression.properties) {
  88. const {key} = property;
  89. if (!key) {
  90. continue;
  91. }
  92. if (propertyKeysEqual(key, specialProtoPropertyKey)) {
  93. continue;
  94. }
  95. const nextPath = [...path, key];
  96. const nextReferences = references
  97. .map(reference => {
  98. const {parent} = reference.identifier;
  99. if (reference.init) {
  100. if (
  101. parent.type === 'VariableDeclarator'
  102. && parent.parent.type === 'VariableDeclaration'
  103. && parent.parent.parent.type === 'ExportNamedDeclaration'
  104. ) {
  105. return {identifier: parent};
  106. }
  107. return;
  108. }
  109. if (parent.type === 'MemberExpression') {
  110. if (
  111. isMemberExpressionAssignment(parent)
  112. || isMemberExpressionCall(parent)
  113. || isMemberExpressionComputedBeyondPrediction(parent)
  114. || propertyKeysEqual(parent.property, key)
  115. ) {
  116. return {identifier: parent};
  117. }
  118. return;
  119. }
  120. if (
  121. parent.type === 'VariableDeclarator'
  122. && parent.id.type === 'ObjectPattern'
  123. ) {
  124. if (objectPatternMatchesObjectExprPropertyKey(parent.id, key)) {
  125. return {identifier: parent};
  126. }
  127. return;
  128. }
  129. if (
  130. parent.type === 'AssignmentExpression'
  131. && parent.left.type === 'ObjectPattern'
  132. ) {
  133. if (objectPatternMatchesObjectExprPropertyKey(parent.left, key)) {
  134. return {identifier: parent};
  135. }
  136. return;
  137. }
  138. return reference;
  139. })
  140. .filter(Boolean);
  141. checkProperty(property, nextReferences, nextPath);
  142. }
  143. };
  144. const checkObject = (declaratorOrProperty, references, path) => {
  145. if (isLeafDeclaratorOrProperty(declaratorOrProperty)) {
  146. return;
  147. }
  148. const value = getDeclaratorOrPropertyValue(declaratorOrProperty);
  149. checkProperties(value, references, path);
  150. };
  151. const checkVariable = variable => {
  152. if (variable.defs.length !== 1) {
  153. return;
  154. }
  155. if (isUnusedVariable(variable)) {
  156. return;
  157. }
  158. const [definition] = variable.defs;
  159. checkObject(definition.node, variable.references);
  160. };
  161. const checkVariables = scope => {
  162. for (const variable of scope.variables) {
  163. checkVariable(variable);
  164. }
  165. };
  166. return {
  167. 'Program:exit'() {
  168. const scopes = getScopes(context.getScope());
  169. for (const scope of scopes) {
  170. if (scope.type === 'global') {
  171. continue;
  172. }
  173. checkVariables(scope);
  174. }
  175. },
  176. };
  177. };
  178. /** @type {import('eslint').Rule.RuleModule} */
  179. module.exports = {
  180. create,
  181. meta: {
  182. type: 'suggestion',
  183. docs: {
  184. description: 'Disallow unused object properties.',
  185. },
  186. messages,
  187. },
  188. };