text-encoding-identifier-case.js 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. 'use strict';
  2. const {replaceStringLiteral} = require('./fix/index.js');
  3. const MESSAGE_ID_ERROR = 'text-encoding-identifier/error';
  4. const MESSAGE_ID_SUGGESTION = 'text-encoding-identifier/suggestion';
  5. const messages = {
  6. [MESSAGE_ID_ERROR]: 'Prefer `{{replacement}}` over `{{value}}`.',
  7. [MESSAGE_ID_SUGGESTION]: 'Replace `{{value}}` with `{{replacement}}`.',
  8. };
  9. const getReplacement = encoding => {
  10. switch (encoding.toLowerCase()) {
  11. // eslint-disable-next-line unicorn/text-encoding-identifier-case
  12. case 'utf-8':
  13. case 'utf8': {
  14. return 'utf8';
  15. }
  16. case 'ascii': {
  17. return 'ascii';
  18. }
  19. // No default
  20. }
  21. };
  22. // `fs.{readFile,readFileSync}()`
  23. const isFsReadFileEncoding = node =>
  24. node.parent.type === 'CallExpression'
  25. && !node.parent.optional
  26. && node.parent.arguments[1] === node
  27. && node.parent.arguments[0].type !== 'SpreadElement'
  28. && node.parent.callee.type === 'MemberExpression'
  29. && !node.parent.callee.optional
  30. && !node.parent.callee.computed
  31. && node.parent.callee.property.type === 'Identifier'
  32. && (node.parent.callee.property.name === 'readFile' || node.parent.callee.property.name === 'readFileSync');
  33. /** @param {import('eslint').Rule.RuleContext} context */
  34. const create = () => ({
  35. Literal(node) {
  36. if (typeof node.value !== 'string') {
  37. return;
  38. }
  39. if (
  40. // eslint-disable-next-line unicorn/text-encoding-identifier-case
  41. node.value === 'utf-8'
  42. && node.parent.type === 'JSXAttribute'
  43. && node.parent.value === node
  44. && node.parent.name.type === 'JSXIdentifier'
  45. && node.parent.name.name.toLowerCase() === 'charset'
  46. && node.parent.parent.type === 'JSXOpeningElement'
  47. && node.parent.parent.attributes.includes(node.parent)
  48. && node.parent.parent.name.type === 'JSXIdentifier'
  49. && node.parent.parent.name.name.toLowerCase() === 'meta'
  50. ) {
  51. return;
  52. }
  53. const {raw} = node;
  54. const value = raw.slice(1, -1);
  55. const replacement = getReplacement(value);
  56. if (!replacement || replacement === value) {
  57. return;
  58. }
  59. /** @param {import('eslint').Rule.RuleFixer} fixer */
  60. const fix = fixer => replaceStringLiteral(fixer, node, replacement);
  61. const problem = {
  62. node,
  63. messageId: MESSAGE_ID_ERROR,
  64. data: {
  65. value,
  66. replacement,
  67. },
  68. };
  69. if (isFsReadFileEncoding(node)) {
  70. problem.fix = fix;
  71. return problem;
  72. }
  73. problem.suggest = [
  74. {
  75. messageId: MESSAGE_ID_SUGGESTION,
  76. fix: fixer => replaceStringLiteral(fixer, node, replacement),
  77. },
  78. ];
  79. return problem;
  80. },
  81. });
  82. /** @type {import('eslint').Rule.RuleModule} */
  83. module.exports = {
  84. create,
  85. meta: {
  86. type: 'suggestion',
  87. docs: {
  88. description: 'Enforce consistent case for text encoding identifiers.',
  89. },
  90. fixable: 'code',
  91. hasSuggestions: true,
  92. messages,
  93. },
  94. };