no-literal-string.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. /**
  2. * @fileoverview disallow literal string
  3. * @author edvardchen
  4. */
  5. 'use strict';
  6. const _ = require('lodash');
  7. const {
  8. isUpperCase,
  9. getNearestAncestor,
  10. isAllowedDOMAttr,
  11. shouldSkip,
  12. } = require('../helper');
  13. function isValidFunctionCall(context, options, { callee }) {
  14. if (callee.type === 'Import') return true;
  15. const sourceText = context.getSourceCode().getText(callee);
  16. return shouldSkip(options.callees, sourceText);
  17. }
  18. function isValidLiteral(options, { value }) {
  19. if (typeof value !== 'string') {
  20. return true;
  21. }
  22. const trimed = value.trim();
  23. if (!trimed) return true;
  24. if (shouldSkip(options.words, trimed)) return true;
  25. }
  26. //------------------------------------------------------------------------------
  27. // Rule Definition
  28. //------------------------------------------------------------------------------
  29. module.exports = {
  30. meta: {
  31. docs: {
  32. description: 'disallow literal string',
  33. category: 'Best Practices',
  34. recommended: true,
  35. },
  36. schema: [require('../options/schema.json')],
  37. },
  38. create(context) {
  39. // variables should be defined here
  40. const { parserServices } = context;
  41. const options = _.defaults(
  42. {},
  43. context.options[0],
  44. require('../options/defaults')
  45. );
  46. const {
  47. mode,
  48. 'should-validate-template': validateTemplate,
  49. message,
  50. } = options;
  51. const onlyValidateJSX = ['jsx-only', 'jsx-text-only'].includes(mode);
  52. //----------------------------------------------------------------------
  53. // Helpers
  54. //----------------------------------------------------------------------
  55. const indicatorStack = [];
  56. function endIndicator() {
  57. indicatorStack.pop();
  58. }
  59. /**
  60. * detect if current "scope" is valid
  61. */
  62. function isValidScope() {
  63. return indicatorStack.some(item => item);
  64. }
  65. //----------------------------------------------------------------------
  66. // Public
  67. //----------------------------------------------------------------------
  68. function report(node) {
  69. context.report({
  70. node,
  71. message: `${message}: ${context.getSourceCode().getText(node.parent)}`,
  72. });
  73. }
  74. function validateBeforeReport(node) {
  75. if (isValidScope()) return;
  76. if (isValidLiteral(options, node)) return;
  77. report(node);
  78. }
  79. function filterOutJSX(node) {
  80. if (onlyValidateJSX) {
  81. const isInsideJSX = context
  82. .getAncestors()
  83. .some(item => ['JSXElement', 'JSXFragment'].includes(item.type));
  84. if (!isInsideJSX) return true;
  85. if (
  86. mode === 'jsx-text-only' &&
  87. !['JSXElement', 'JSXFragment'].includes(node.parent.type)
  88. ) {
  89. // Under mode jsx-text-only, if the direct parent isn't JSXElement or JSXFragment then skip
  90. return true;
  91. }
  92. }
  93. return false;
  94. }
  95. const scriptVisitor = {
  96. //
  97. // ─── EXPORT AND IMPORT ───────────────────────────────────────────
  98. //
  99. ImportExpression(node) {
  100. // allow (import('abc'))
  101. indicatorStack.push(true);
  102. },
  103. 'ImportExpression:exit': endIndicator,
  104. ImportDeclaration(node) {
  105. // allow (import abc form 'abc')
  106. indicatorStack.push(true);
  107. },
  108. 'ImportDeclaration:exit': endIndicator,
  109. ExportAllDeclaration(node) {
  110. // allow export * from 'mod'
  111. indicatorStack.push(true);
  112. },
  113. 'ExportAllDeclaration:exit': endIndicator,
  114. 'ExportNamedDeclaration[source]'(node) {
  115. // allow export { named } from 'mod'
  116. indicatorStack.push(true);
  117. },
  118. 'ExportNamedDeclaration[source]:exit': endIndicator,
  119. // ─────────────────────────────────────────────────────────────────
  120. //
  121. // ─── JSX ─────────────────────────────────────────────────────────
  122. //
  123. JSXElement(node) {
  124. indicatorStack.push(
  125. shouldSkip(options['jsx-components'], node.openingElement.name.name)
  126. );
  127. },
  128. 'JSXElement:exit': endIndicator,
  129. JSXAttribute(node) {
  130. const attrName = node.name.name;
  131. // allow <MyComponent className="active" />
  132. if (shouldSkip(options['jsx-attributes'], attrName)) {
  133. indicatorStack.push(true);
  134. return;
  135. }
  136. const jsxElement = getNearestAncestor(node, 'JSXOpeningElement');
  137. const tagName = jsxElement.name.name;
  138. if (isAllowedDOMAttr(tagName, attrName)) {
  139. indicatorStack.push(true);
  140. return;
  141. }
  142. indicatorStack.push(false);
  143. },
  144. 'JSXAttribute:exit': endIndicator,
  145. // @typescript-eslint/parser would parse string literal as JSXText node
  146. JSXText(node) {
  147. validateBeforeReport(node);
  148. },
  149. // ─────────────────────────────────────────────────────────────────
  150. //
  151. // ─── TYPESCRIPT ──────────────────────────────────────────────────
  152. //
  153. TSModuleDeclaration() {
  154. indicatorStack.push(true);
  155. },
  156. 'TSModuleDeclaration:exit': endIndicator,
  157. TSLiteralType(node) {
  158. // allow var a: Type['member'];
  159. indicatorStack.push(true);
  160. },
  161. 'TSLiteralType:exit': endIndicator,
  162. TSEnumMember(node) {
  163. // allow enum E { "a b" = 1 }
  164. indicatorStack.push(true);
  165. },
  166. 'TSEnumMember:exit': endIndicator,
  167. // ─────────────────────────────────────────────────────────────────
  168. ClassProperty(node) {
  169. indicatorStack.push(
  170. !!(node.key && shouldSkip(options['class-properties'], node.key.name))
  171. );
  172. },
  173. 'ClassProperty:exit': endIndicator,
  174. VariableDeclarator(node) {
  175. // allow statements like const A_B = "test"
  176. indicatorStack.push(isUpperCase(node.id.name));
  177. },
  178. 'VariableDeclarator:exit': endIndicator,
  179. Property(node) {
  180. // pick up key.name if key is Identifier or key.value if key is Literal
  181. // dont care whether if this is computed or not
  182. const result = shouldSkip(
  183. options['object-properties'],
  184. node.key.name || node.key.value
  185. );
  186. indicatorStack.push(result);
  187. },
  188. 'Property:exit': endIndicator,
  189. BinaryExpression(node) {
  190. const { operator } = node;
  191. // allow name === 'Android'
  192. indicatorStack.push(operator !== '+');
  193. },
  194. 'BinaryExpression:exit': endIndicator,
  195. AssignmentPattern(node) {
  196. // allow function bar(input = 'foo') {}
  197. indicatorStack.push(true);
  198. },
  199. 'AssignmentPattern:exit': endIndicator,
  200. NewExpression(node) {
  201. indicatorStack.push(isValidFunctionCall(context, options, node));
  202. },
  203. 'NewExpression:exit': endIndicator,
  204. CallExpression(node) {
  205. indicatorStack.push(isValidFunctionCall(context, options, node));
  206. },
  207. 'CallExpression:exit': endIndicator,
  208. 'SwitchCase > Literal'(node) {
  209. indicatorStack.push(true);
  210. },
  211. 'SwitchCase > Literal:exit': endIndicator,
  212. 'AssignmentExpression[left.type="MemberExpression"]'(node) {
  213. // allow Enum['value']
  214. indicatorStack.push(
  215. shouldSkip(options['object-properties'], node.left.property.name)
  216. );
  217. },
  218. 'AssignmentExpression[left.type="MemberExpression"]:exit'(node) {
  219. endIndicator();
  220. },
  221. 'MemberExpression > Literal'(node) {
  222. // allow Enum['value']
  223. indicatorStack.push(true);
  224. },
  225. 'MemberExpression > Literal:exit'(node) {
  226. endIndicator();
  227. },
  228. TemplateLiteral(node) {
  229. if (!validateTemplate) {
  230. return;
  231. }
  232. if (filterOutJSX(node)) {
  233. return;
  234. }
  235. if (isValidScope()) return;
  236. const { quasis = [] } = node;
  237. quasis.some(({ value: { raw } }) => {
  238. if (isValidLiteral(options, { value: raw })) return;
  239. report(node);
  240. return true; // break
  241. });
  242. },
  243. 'Literal:exit'(node) {
  244. if (filterOutJSX(node)) {
  245. return;
  246. }
  247. // ignore `var a = { "foo": 123 }`
  248. if (node.parent.key === node) {
  249. return;
  250. }
  251. validateBeforeReport(node);
  252. },
  253. };
  254. return (
  255. (parserServices.defineTemplateBodyVisitor &&
  256. parserServices.defineTemplateBodyVisitor(
  257. {
  258. VText(node) {
  259. scriptVisitor['JSXText'](node);
  260. },
  261. 'VExpressionContainer CallExpression'(node) {
  262. scriptVisitor['CallExpression'](node);
  263. },
  264. 'VExpressionContainer CallExpression:exit'(node) {
  265. scriptVisitor['CallExpression:exit'](node);
  266. },
  267. 'VExpressionContainer Literal:exit'(node) {
  268. scriptVisitor['Literal:exit'](node);
  269. },
  270. },
  271. scriptVisitor
  272. )) ||
  273. scriptVisitor
  274. );
  275. },
  276. };