quote-props.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. /**
  2. * @fileoverview Rule to flag non-quoted property names in object literals.
  3. * @author Mathias Bynens <http://mathiasbynens.be/>
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const espree = require("espree");
  10. const astUtils = require("./utils/ast-utils");
  11. const keywords = require("./utils/keywords");
  12. //------------------------------------------------------------------------------
  13. // Rule Definition
  14. //------------------------------------------------------------------------------
  15. /** @type {import('../shared/types').Rule} */
  16. module.exports = {
  17. meta: {
  18. type: "suggestion",
  19. docs: {
  20. description: "Require quotes around object literal property names",
  21. recommended: false,
  22. url: "https://eslint.org/docs/rules/quote-props"
  23. },
  24. schema: {
  25. anyOf: [
  26. {
  27. type: "array",
  28. items: [
  29. {
  30. enum: ["always", "as-needed", "consistent", "consistent-as-needed"]
  31. }
  32. ],
  33. minItems: 0,
  34. maxItems: 1
  35. },
  36. {
  37. type: "array",
  38. items: [
  39. {
  40. enum: ["always", "as-needed", "consistent", "consistent-as-needed"]
  41. },
  42. {
  43. type: "object",
  44. properties: {
  45. keywords: {
  46. type: "boolean"
  47. },
  48. unnecessary: {
  49. type: "boolean"
  50. },
  51. numbers: {
  52. type: "boolean"
  53. }
  54. },
  55. additionalProperties: false
  56. }
  57. ],
  58. minItems: 0,
  59. maxItems: 2
  60. }
  61. ]
  62. },
  63. fixable: "code",
  64. messages: {
  65. requireQuotesDueToReservedWord: "Properties should be quoted as '{{property}}' is a reserved word.",
  66. inconsistentlyQuotedProperty: "Inconsistently quoted property '{{key}}' found.",
  67. unnecessarilyQuotedProperty: "Unnecessarily quoted property '{{property}}' found.",
  68. unquotedReservedProperty: "Unquoted reserved word '{{property}}' used as key.",
  69. unquotedNumericProperty: "Unquoted number literal '{{property}}' used as key.",
  70. unquotedPropertyFound: "Unquoted property '{{property}}' found.",
  71. redundantQuoting: "Properties shouldn't be quoted as all quotes are redundant."
  72. }
  73. },
  74. create(context) {
  75. const MODE = context.options[0],
  76. KEYWORDS = context.options[1] && context.options[1].keywords,
  77. CHECK_UNNECESSARY = !context.options[1] || context.options[1].unnecessary !== false,
  78. NUMBERS = context.options[1] && context.options[1].numbers,
  79. sourceCode = context.getSourceCode();
  80. /**
  81. * Checks whether a certain string constitutes an ES3 token
  82. * @param {string} tokenStr The string to be checked.
  83. * @returns {boolean} `true` if it is an ES3 token.
  84. */
  85. function isKeyword(tokenStr) {
  86. return keywords.includes(tokenStr);
  87. }
  88. /**
  89. * Checks if an espree-tokenized key has redundant quotes (i.e. whether quotes are unnecessary)
  90. * @param {string} rawKey The raw key value from the source
  91. * @param {espreeTokens} tokens The espree-tokenized node key
  92. * @param {boolean} [skipNumberLiterals=false] Indicates whether number literals should be checked
  93. * @returns {boolean} Whether or not a key has redundant quotes.
  94. * @private
  95. */
  96. function areQuotesRedundant(rawKey, tokens, skipNumberLiterals) {
  97. return tokens.length === 1 && tokens[0].start === 0 && tokens[0].end === rawKey.length &&
  98. (["Identifier", "Keyword", "Null", "Boolean"].includes(tokens[0].type) ||
  99. (tokens[0].type === "Numeric" && !skipNumberLiterals && String(+tokens[0].value) === tokens[0].value));
  100. }
  101. /**
  102. * Returns a string representation of a property node with quotes removed
  103. * @param {ASTNode} key Key AST Node, which may or may not be quoted
  104. * @returns {string} A replacement string for this property
  105. */
  106. function getUnquotedKey(key) {
  107. return key.type === "Identifier" ? key.name : key.value;
  108. }
  109. /**
  110. * Returns a string representation of a property node with quotes added
  111. * @param {ASTNode} key Key AST Node, which may or may not be quoted
  112. * @returns {string} A replacement string for this property
  113. */
  114. function getQuotedKey(key) {
  115. if (key.type === "Literal" && typeof key.value === "string") {
  116. // If the key is already a string literal, don't replace the quotes with double quotes.
  117. return sourceCode.getText(key);
  118. }
  119. // Otherwise, the key is either an identifier or a number literal.
  120. return `"${key.type === "Identifier" ? key.name : key.value}"`;
  121. }
  122. /**
  123. * Ensures that a property's key is quoted only when necessary
  124. * @param {ASTNode} node Property AST node
  125. * @returns {void}
  126. */
  127. function checkUnnecessaryQuotes(node) {
  128. const key = node.key;
  129. if (node.method || node.computed || node.shorthand) {
  130. return;
  131. }
  132. if (key.type === "Literal" && typeof key.value === "string") {
  133. let tokens;
  134. try {
  135. tokens = espree.tokenize(key.value);
  136. } catch {
  137. return;
  138. }
  139. if (tokens.length !== 1) {
  140. return;
  141. }
  142. const isKeywordToken = isKeyword(tokens[0].value);
  143. if (isKeywordToken && KEYWORDS) {
  144. return;
  145. }
  146. if (CHECK_UNNECESSARY && areQuotesRedundant(key.value, tokens, NUMBERS)) {
  147. context.report({
  148. node,
  149. messageId: "unnecessarilyQuotedProperty",
  150. data: { property: key.value },
  151. fix: fixer => fixer.replaceText(key, getUnquotedKey(key))
  152. });
  153. }
  154. } else if (KEYWORDS && key.type === "Identifier" && isKeyword(key.name)) {
  155. context.report({
  156. node,
  157. messageId: "unquotedReservedProperty",
  158. data: { property: key.name },
  159. fix: fixer => fixer.replaceText(key, getQuotedKey(key))
  160. });
  161. } else if (NUMBERS && key.type === "Literal" && astUtils.isNumericLiteral(key)) {
  162. context.report({
  163. node,
  164. messageId: "unquotedNumericProperty",
  165. data: { property: key.value },
  166. fix: fixer => fixer.replaceText(key, getQuotedKey(key))
  167. });
  168. }
  169. }
  170. /**
  171. * Ensures that a property's key is quoted
  172. * @param {ASTNode} node Property AST node
  173. * @returns {void}
  174. */
  175. function checkOmittedQuotes(node) {
  176. const key = node.key;
  177. if (!node.method && !node.computed && !node.shorthand && !(key.type === "Literal" && typeof key.value === "string")) {
  178. context.report({
  179. node,
  180. messageId: "unquotedPropertyFound",
  181. data: { property: key.name || key.value },
  182. fix: fixer => fixer.replaceText(key, getQuotedKey(key))
  183. });
  184. }
  185. }
  186. /**
  187. * Ensures that an object's keys are consistently quoted, optionally checks for redundancy of quotes
  188. * @param {ASTNode} node Property AST node
  189. * @param {boolean} checkQuotesRedundancy Whether to check quotes' redundancy
  190. * @returns {void}
  191. */
  192. function checkConsistency(node, checkQuotesRedundancy) {
  193. const quotedProps = [],
  194. unquotedProps = [];
  195. let keywordKeyName = null,
  196. necessaryQuotes = false;
  197. node.properties.forEach(property => {
  198. const key = property.key;
  199. if (!key || property.method || property.computed || property.shorthand) {
  200. return;
  201. }
  202. if (key.type === "Literal" && typeof key.value === "string") {
  203. quotedProps.push(property);
  204. if (checkQuotesRedundancy) {
  205. let tokens;
  206. try {
  207. tokens = espree.tokenize(key.value);
  208. } catch {
  209. necessaryQuotes = true;
  210. return;
  211. }
  212. necessaryQuotes = necessaryQuotes || !areQuotesRedundant(key.value, tokens) || KEYWORDS && isKeyword(tokens[0].value);
  213. }
  214. } else if (KEYWORDS && checkQuotesRedundancy && key.type === "Identifier" && isKeyword(key.name)) {
  215. unquotedProps.push(property);
  216. necessaryQuotes = true;
  217. keywordKeyName = key.name;
  218. } else {
  219. unquotedProps.push(property);
  220. }
  221. });
  222. if (checkQuotesRedundancy && quotedProps.length && !necessaryQuotes) {
  223. quotedProps.forEach(property => {
  224. context.report({
  225. node: property,
  226. messageId: "redundantQuoting",
  227. fix: fixer => fixer.replaceText(property.key, getUnquotedKey(property.key))
  228. });
  229. });
  230. } else if (unquotedProps.length && keywordKeyName) {
  231. unquotedProps.forEach(property => {
  232. context.report({
  233. node: property,
  234. messageId: "requireQuotesDueToReservedWord",
  235. data: { property: keywordKeyName },
  236. fix: fixer => fixer.replaceText(property.key, getQuotedKey(property.key))
  237. });
  238. });
  239. } else if (quotedProps.length && unquotedProps.length) {
  240. unquotedProps.forEach(property => {
  241. context.report({
  242. node: property,
  243. messageId: "inconsistentlyQuotedProperty",
  244. data: { key: property.key.name || property.key.value },
  245. fix: fixer => fixer.replaceText(property.key, getQuotedKey(property.key))
  246. });
  247. });
  248. }
  249. }
  250. return {
  251. Property(node) {
  252. if (MODE === "always" || !MODE) {
  253. checkOmittedQuotes(node);
  254. }
  255. if (MODE === "as-needed") {
  256. checkUnnecessaryQuotes(node);
  257. }
  258. },
  259. ObjectExpression(node) {
  260. if (MODE === "consistent") {
  261. checkConsistency(node, false);
  262. }
  263. if (MODE === "consistent-as-needed") {
  264. checkConsistency(node, true);
  265. }
  266. }
  267. };
  268. }
  269. };