.eslintrc.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. module.exports = {
  2. root: true,
  3. parser: '@typescript-eslint/parser',
  4. extends: [
  5. 'eslint:recommended',
  6. 'plugin:@typescript-eslint/recommended',
  7. 'standard',
  8. 'prettier',
  9. ],
  10. plugins: ['@overleaf'],
  11. env: {
  12. es2020: true,
  13. },
  14. settings: {
  15. // Tell eslint-plugin-react to detect which version of React we are using
  16. react: {
  17. version: 'detect',
  18. },
  19. },
  20. rules: {
  21. 'no-constant-binary-expression': 'error',
  22. // do not allow importing of implicit dependencies.
  23. 'import/no-extraneous-dependencies': 'error',
  24. '@overleaf/prefer-kebab-url': 'error',
  25. // disable some TypeScript rules
  26. '@typescript-eslint/no-var-requires': 'off',
  27. '@typescript-eslint/no-unused-vars': 'off',
  28. '@typescript-eslint/no-empty-function': 'off',
  29. '@typescript-eslint/no-explicit-any': 'off',
  30. '@typescript-eslint/no-this-alias': 'off',
  31. '@typescript-eslint/no-non-null-assertion': 'off',
  32. '@typescript-eslint/ban-ts-comment': 'off',
  33. 'no-use-before-define': 'off',
  34. '@typescript-eslint/no-use-before-define': [
  35. 'error',
  36. { functions: false, classes: false, variables: false },
  37. ],
  38. },
  39. overrides: [
  40. // NOTE: changing paths may require updating them in the Makefile too.
  41. {
  42. // Node
  43. files: [
  44. '**/app/src/**/*.{js,mjs}',
  45. 'app.{js,mjs}',
  46. 'i18next-scanner.config.js',
  47. 'scripts/**/*.{js,mjs}',
  48. 'webpack.config*.js',
  49. ],
  50. env: {
  51. node: true,
  52. },
  53. },
  54. {
  55. // Test specific rules
  56. files: ['**/test/**/*.*'],
  57. plugins: ['mocha', 'chai-expect', 'chai-friendly'],
  58. env: {
  59. mocha: true,
  60. },
  61. rules: {
  62. // mocha-specific rules
  63. 'mocha/handle-done-callback': 'error',
  64. 'mocha/no-exclusive-tests': 'error',
  65. 'mocha/no-global-tests': 'error',
  66. 'mocha/no-identical-title': 'error',
  67. 'mocha/no-nested-tests': 'error',
  68. 'mocha/no-pending-tests': 'error',
  69. 'mocha/no-skipped-tests': 'error',
  70. 'mocha/no-mocha-arrows': 'error',
  71. // Swap the no-unused-expressions rule with a more chai-friendly one
  72. 'no-unused-expressions': 'off',
  73. 'chai-friendly/no-unused-expressions': 'error',
  74. // chai-specific rules
  75. 'chai-expect/missing-assertion': 'error',
  76. 'chai-expect/terminating-properties': 'error',
  77. // prefer-arrow-callback applies to all callbacks, not just ones in mocha tests.
  78. // we don't enforce this at the top-level - just in tests to manage `this` scope
  79. // based on mocha's context mechanism
  80. 'mocha/prefer-arrow-callback': 'error',
  81. '@typescript-eslint/no-unused-expressions': 'off',
  82. },
  83. },
  84. {
  85. // ES specific rules
  86. files: [
  87. '**/app/src/**/*.mjs',
  88. 'modules/*/index.mjs',
  89. 'app.mjs',
  90. 'scripts/**/*.mjs',
  91. 'migrations/**/*.mjs',
  92. ],
  93. excludedFiles: [
  94. // migration template file
  95. 'migrations/lib/template.mjs',
  96. ],
  97. parserOptions: {
  98. sourceType: 'module',
  99. },
  100. plugins: ['unicorn'],
  101. rules: {
  102. 'import/no-unresolved': [
  103. 'error',
  104. {
  105. // eslint-plugin-import does not support exports directive in package.json
  106. // https://github.com/import-js/eslint-plugin-import/issues/1810
  107. ignore: ['^p-queue$'],
  108. },
  109. ],
  110. 'import/extensions': [
  111. 'error',
  112. 'ignorePackages',
  113. {
  114. js: 'always',
  115. mjs: 'always',
  116. },
  117. ],
  118. 'unicorn/prefer-module': 'error',
  119. 'unicorn/prefer-node-protocol': 'error',
  120. },
  121. },
  122. {
  123. // Backend specific rules
  124. files: ['**/app/src/**/*.{js,mjs}', 'app.{js,mjs}'],
  125. parserOptions: {
  126. tsconfigRootDir: __dirname,
  127. project: './tsconfig.backend.json',
  128. },
  129. rules: {
  130. // do not allow importing of implicit dependencies.
  131. 'import/no-extraneous-dependencies': [
  132. 'error',
  133. {
  134. // do not allow importing of devDependencies.
  135. devDependencies: false,
  136. },
  137. ],
  138. 'no-restricted-syntax': [
  139. 'error',
  140. // do not allow node-fetch in backend code
  141. {
  142. selector:
  143. "CallExpression[callee.name='require'] > .arguments[value='node-fetch']",
  144. message:
  145. 'Requiring node-fetch is not allowed in production services, please use fetch-utils.',
  146. },
  147. // mongoose populate must set fields to populate
  148. {
  149. selector:
  150. "CallExpression[callee.property.name='populate'][arguments.length<2]",
  151. message:
  152. "Populate without a second argument returns the whole document. Use populate('field',['prop1','prop2']) instead",
  153. },
  154. // Require `new` when constructing ObjectId (For mongo + mongoose upgrade)
  155. {
  156. selector:
  157. "CallExpression[callee.name='ObjectId'], CallExpression[callee.property.name='ObjectId']",
  158. message:
  159. 'Construct ObjectId with `new ObjectId()` instead of `ObjectId()`',
  160. },
  161. // Require `new` when mapping a list of ids to a list of ObjectId (For mongo + mongoose upgrade)
  162. {
  163. selector:
  164. "CallExpression[callee.property.name='map'] Identifier[name='ObjectId']:first-child, CallExpression[callee.property.name='map'] MemberExpression[property.name='ObjectId']:first-child",
  165. message:
  166. "Don't map ObjectId directly. Use `id => new ObjectId(id)` instead",
  167. },
  168. // Catch incorrect usage of `await db.collection.find()`
  169. {
  170. selector:
  171. "AwaitExpression > CallExpression > MemberExpression[property.name='find'][object.object.name='db']",
  172. message:
  173. 'Mongo find returns a cursor not a promise, use `for await (const result of cursor)` or `.toArray()` instead.',
  174. },
  175. ],
  176. '@typescript-eslint/no-floating-promises': [
  177. 'error',
  178. { checkThenables: true },
  179. ],
  180. },
  181. },
  182. {
  183. // Backend scripts specific rules
  184. files: ['**/scripts/**/*.js'],
  185. rules: {
  186. 'no-restricted-syntax': [
  187. 'error',
  188. // Require `new` when constructing ObjectId (For mongo + mongoose upgrade)
  189. {
  190. selector:
  191. "CallExpression[callee.name='ObjectId'], CallExpression[callee.property.name='ObjectId']",
  192. message:
  193. 'Construct ObjectId with `new ObjectId()` instead of `ObjectId()`',
  194. },
  195. // Require `new` when mapping a list of ids to a list of ObjectId (For mongo + mongoose upgrade)
  196. {
  197. selector:
  198. "CallExpression[callee.property.name='map'] Identifier[name='ObjectId']:first-child, CallExpression[callee.property.name='map'] MemberExpression[property.name='ObjectId']:first-child",
  199. message:
  200. "Don't map ObjectId directly. Use `id => new ObjectId(id)` instead",
  201. },
  202. // Catch incorrect usage of `await db.collection.find()`
  203. {
  204. selector:
  205. "AwaitExpression > CallExpression > MemberExpression[property.name='find'][object.object.name='db']",
  206. message:
  207. 'Mongo find returns a cursor not a promise, use `for await (const result of cursor)` or `.toArray()` instead.',
  208. },
  209. ],
  210. },
  211. },
  212. {
  213. // Cypress specific rules
  214. files: [
  215. 'cypress/**/*.{js,jsx,ts,tsx}',
  216. '**/test/frontend/**/*.spec.{js,jsx,ts,tsx}',
  217. ],
  218. extends: ['plugin:cypress/recommended'],
  219. },
  220. {
  221. // Frontend test specific rules
  222. files: ['**/frontend/**/*.test.{js,jsx,ts,tsx}'],
  223. plugins: ['testing-library'],
  224. extends: ['plugin:testing-library/react'],
  225. rules: {
  226. 'testing-library/no-await-sync-events': 'off',
  227. 'testing-library/no-await-sync-queries': 'off',
  228. 'testing-library/no-container': 'off',
  229. 'testing-library/no-node-access': 'off',
  230. 'testing-library/no-render-in-lifecycle': 'off',
  231. 'testing-library/no-wait-for-multiple-assertions': 'off',
  232. 'testing-library/no-wait-for-side-effects': 'off',
  233. 'testing-library/prefer-query-by-disappearance': 'off',
  234. 'testing-library/prefer-screen-queries': 'off',
  235. 'testing-library/render-result-naming-convention': 'off',
  236. },
  237. },
  238. {
  239. // Frontend specific rules
  240. files: [
  241. '**/frontend/js/**/*.{js,jsx,ts,tsx}',
  242. '**/frontend/stories/**/*.{js,jsx,ts,tsx}',
  243. '**/*.stories.{js,jsx,ts,tsx}',
  244. '**/test/frontend/**/*.{js,jsx,ts,tsx}',
  245. '**/test/frontend/components/**/*.spec.{js,jsx,ts,tsx}',
  246. ],
  247. env: {
  248. browser: true,
  249. },
  250. parserOptions: {
  251. sourceType: 'module',
  252. },
  253. plugins: ['jsx-a11y'],
  254. extends: [
  255. 'plugin:react/recommended',
  256. 'plugin:react-hooks/recommended',
  257. 'plugin:jsx-a11y/recommended',
  258. 'standard-jsx',
  259. 'prettier',
  260. ],
  261. globals: {
  262. __webpack_public_path__: true,
  263. $: true,
  264. ga: true,
  265. },
  266. rules: {
  267. // TODO: remove once https://github.com/standard/eslint-config-standard-react/issues/68 (support eslint@8) is fixed.
  268. // START: inline standard-react rules
  269. // "react/jsx-no-bind": ["error", {
  270. // "allowArrowFunctions": true,
  271. // "allowBind": false,
  272. // "ignoreRefs": true
  273. // },],
  274. 'react/no-did-update-set-state': 'error',
  275. 'react/no-unused-prop-types': 'error',
  276. 'react/prop-types': 'error',
  277. // "react/react-in-jsx-scope": "error",
  278. // END: inline standard-react rules
  279. 'react/no-unknown-property': [
  280. 'error',
  281. {
  282. ignore: ['dnd-container', 'dropdown-toggle'],
  283. },
  284. ],
  285. 'react/jsx-no-target-blank': [
  286. 'error',
  287. {
  288. allowReferrer: true,
  289. },
  290. ],
  291. // Prevent usage of legacy string refs
  292. 'react/no-string-refs': 'error',
  293. // Prevent curly braces around strings (as they're unnecessary)
  294. 'react/jsx-curly-brace-presence': [
  295. 'error',
  296. {
  297. props: 'never',
  298. children: 'never',
  299. },
  300. ],
  301. // Don't import React for JSX; the JSX runtime is added by a Babel plugin
  302. 'react/react-in-jsx-scope': 'off',
  303. 'react/jsx-uses-react': 'off',
  304. // Allow functions as JSX props
  305. 'react/jsx-no-bind': 'off', // TODO: fix occurrences and re-enable this
  306. // Fix conflict between prettier & standard by overriding to prefer
  307. // double quotes
  308. 'jsx-quotes': ['error', 'prefer-double'],
  309. // Override weird behaviour of jsx-a11y label-has-for (says labels must be
  310. // nested *and* have for/id attributes)
  311. 'jsx-a11y/label-has-for': [
  312. 'error',
  313. {
  314. required: {
  315. some: ['nesting', 'id'],
  316. },
  317. },
  318. ],
  319. // Require .jsx or .tsx file extension when using JSX
  320. 'react/jsx-filename-extension': [
  321. 'error',
  322. {
  323. extensions: ['.jsx', '.tsx'],
  324. },
  325. ],
  326. 'no-restricted-syntax': [
  327. 'error',
  328. // prohibit direct calls to methods of window.localStorage
  329. {
  330. selector:
  331. "CallExpression[callee.object.object.name='window'][callee.object.property.name='localStorage']",
  332. message:
  333. 'Modify location via customLocalStorage instead of calling window.localStorage methods directly',
  334. },
  335. ],
  336. },
  337. },
  338. {
  339. // Sorting for Meta
  340. files: ['frontend/js/utils/meta.ts'],
  341. rules: {
  342. '@typescript-eslint/member-ordering': [
  343. 'error',
  344. { interfaces: { order: 'alphabetically' } },
  345. ],
  346. },
  347. },
  348. {
  349. // React component specific rules
  350. //
  351. files: [
  352. '**/frontend/js/**/components/**/*.{js,jsx,ts,tsx}',
  353. '**/frontend/js/**/hooks/**/*.{js,jsx,ts,tsx}',
  354. ],
  355. rules: {
  356. '@overleaf/no-unnecessary-trans': 'error',
  357. '@overleaf/should-unescape-trans': 'error',
  358. // https://astexplorer.net/
  359. 'no-restricted-syntax': [
  360. 'error',
  361. // prohibit direct calls to methods of window.location
  362. {
  363. selector:
  364. "CallExpression[callee.object.object.name='window'][callee.object.property.name='location']",
  365. message:
  366. 'Modify location via useLocation instead of calling window.location methods directly',
  367. },
  368. // prohibit assignment to window.location
  369. {
  370. selector:
  371. "AssignmentExpression[left.object.name='window'][left.property.name='location']",
  372. message:
  373. 'Modify location via useLocation instead of calling window.location methods directly',
  374. },
  375. // prohibit assignment to window.location.href
  376. {
  377. selector:
  378. "AssignmentExpression[left.object.object.name='window'][left.object.property.name='location'][left.property.name='href']",
  379. message:
  380. 'Modify location via useLocation instead of calling window.location methods directly',
  381. },
  382. // prohibit using lookbehinds due to incidents with Safari simply crashing when the script is parsed
  383. {
  384. selector: 'Literal[regex.pattern=/\\(\\?<[!=]/]',
  385. message: 'Lookbehind is not supported in older Safari versions.',
  386. },
  387. // prohibit direct calls to methods of window.localStorage
  388. // NOTE: this rule is also defined for all frontend files, but those rules are overriden by the React component-specific config
  389. {
  390. selector:
  391. "CallExpression[callee.object.object.name='window'][callee.object.property.name='localStorage']",
  392. message:
  393. 'Modify location via customLocalStorage instead of calling window.localStorage methods directly',
  394. },
  395. ],
  396. },
  397. },
  398. // React + TypeScript-specific rules
  399. {
  400. files: ['**/*.tsx'],
  401. rules: {
  402. 'react/prop-types': 'off',
  403. 'no-undef': 'off',
  404. },
  405. },
  406. // TypeScript-specific rules
  407. {
  408. files: ['**/*.ts'],
  409. rules: {
  410. 'no-undef': 'off',
  411. },
  412. },
  413. // JavaScript-specific rules
  414. {
  415. files: ['**/*.js'],
  416. rules: {
  417. '@typescript-eslint/no-require-imports': 'off',
  418. },
  419. },
  420. {
  421. files: ['scripts/ukamf/*.js'],
  422. rules: {
  423. // Do not allow importing of any dependencies unless specified in either
  424. // - web/package.json
  425. // - web/scripts/ukamf/package.json
  426. 'import/no-extraneous-dependencies': [
  427. 'error',
  428. { packageDir: ['.', 'scripts/ukamf'] },
  429. ],
  430. },
  431. },
  432. {
  433. files: ['scripts/learn/checkSanitize/*.js'],
  434. rules: {
  435. // The checkSanitize script is used in the dev-env only.
  436. 'import/no-extraneous-dependencies': [
  437. 'error',
  438. {
  439. devDependencies: true,
  440. packageDir: ['.', '../../'],
  441. },
  442. ],
  443. },
  444. },
  445. {
  446. files: [
  447. // Backend: Use @overleaf/logger
  448. // Docs: https://manual.dev-overleaf.com/development/code/logging/#structured-logging
  449. '**/app/**/*.{js,cjs,mjs}',
  450. 'app.{js,mjs}',
  451. 'modules/*/*.{js,mjs}',
  452. // Frontend: Prefer debugConsole over bare console
  453. // Docs: https://manual.dev-overleaf.com/development/code/logging/#frontend
  454. '**/frontend/**/*.{js,jsx,ts,tsx}',
  455. // Tests
  456. '**/test/**/*.{js,cjs,mjs,jsx,ts,tsx}',
  457. ],
  458. excludedFiles: [
  459. // Allow console logs in scripts
  460. '**/scripts/**/*.js',
  461. // Allow console logs in stories
  462. '**/stories/**/*.{js,jsx,ts,tsx}',
  463. // Workers do not have access to the search params for enabling ?debug=true.
  464. // self.location.url is the URL of the worker script.
  465. '*.worker.{js,ts}',
  466. ],
  467. rules: {
  468. 'no-console': 'error',
  469. },
  470. },
  471. ],
  472. }