quote-props.js 9.9 KB

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