.eslintrc.js 18 KB

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