no-unused-properties.js 5.1 KB

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