overleaf/services/web/.eslintrc.js
Antoine Clausse 3d5418611b Add an ESLint rule enforcing kebab-case in URL paths (#18913)
* Add a tsconfig.json to the eslint-plugin folder so it can be linted too

* Create eslint rule `prefer-kebab-url`

* Add prefer-kebab-url to service/web

* Ignore lowercased strings

Prevents from trying to change `v0` to `v-0`

* Ignore parts between brackets or parenthesis

* Record in URL paths by case

lower: 241
kebab: 60
snake: 43
camel: 22
other: 5

* Revert "Record in URL paths by case"

This reverts commit 262f483aafb0daa1a01e9025488cdc33f31ef67c.

* Showcase how the autofix would change the code

* Revert "Showcase how the autofix would change the code"

This reverts commit f045292bf01623de2df5b89fc0d68737a39913dc.

* Add ignored words so we don't have to update old code

* Change the rule so it suggests instead of fixing

This prevents eslint autofix from creating bugs

* Move list of ignored words to its own file

Per https://github.com/overleaf/internal/pull/18913#discussion_r1644204034

* Add comment explaining the ignore list

GitOrigin-RevId: d13918b0bcba9d77120f1b61b354a79dc8fb4b4e
2024-06-24 12:04:26 +00:00

431 lines
15 KiB
JavaScript

module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'standard',
'prettier',
],
plugins: ['@overleaf'],
env: {
es2020: true,
},
settings: {
// Tell eslint-plugin-react to detect which version of React we are using
react: {
version: 'detect',
},
},
rules: {
'no-constant-binary-expression': 'error',
// do not allow importing of implicit dependencies.
'import/no-extraneous-dependencies': 'error',
'@overleaf/prefer-kebab-url': 'error',
// disable some TypeScript rules
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-this-alias': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
},
overrides: [
// NOTE: changing paths may require updating them in the Makefile too.
{
// Node
files: [
'**/app/src/**/*.js',
'app.js',
'i18next-scanner.config.js',
'scripts/**/*.js',
'webpack.config*.js',
],
env: {
node: true,
},
},
{
// Test specific rules
files: ['**/test/**/*.*'],
plugins: ['mocha', 'chai-expect', 'chai-friendly'],
env: {
mocha: true,
},
rules: {
// mocha-specific rules
'mocha/handle-done-callback': 'error',
'mocha/no-exclusive-tests': 'error',
'mocha/no-global-tests': 'error',
'mocha/no-identical-title': 'error',
'mocha/no-nested-tests': 'error',
'mocha/no-pending-tests': 'error',
'mocha/no-skipped-tests': 'error',
'mocha/no-mocha-arrows': 'error',
// Swap the no-unused-expressions rule with a more chai-friendly one
'no-unused-expressions': 'off',
'chai-friendly/no-unused-expressions': 'error',
// chai-specific rules
'chai-expect/missing-assertion': 'error',
'chai-expect/terminating-properties': 'error',
// prefer-arrow-callback applies to all callbacks, not just ones in mocha tests.
// we don't enforce this at the top-level - just in tests to manage `this` scope
// based on mocha's context mechanism
'mocha/prefer-arrow-callback': 'error',
},
},
{
// Backend specific rules
files: ['**/app/src/**/*.js', 'app.js'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.backend.json',
},
rules: {
// do not allow importing of implicit dependencies.
'import/no-extraneous-dependencies': [
'error',
{
// do not allow importing of devDependencies.
devDependencies: false,
},
],
'no-restricted-syntax': [
'error',
// do not allow node-fetch in backend code
{
selector:
"CallExpression[callee.name='require'] > .arguments[value='node-fetch']",
message:
'Requiring node-fetch is not allowed in production services, please use fetch-utils.',
},
// mongoose populate must set fields to populate
{
selector:
"CallExpression[callee.property.name='populate'][arguments.length<2]",
message:
"Populate without a second argument returns the whole document. Use populate('field',['prop1','prop2']) instead",
},
// Require `new` when constructing ObjectId (For mongo + mongoose upgrade)
{
selector:
"CallExpression[callee.name='ObjectId'], CallExpression[callee.property.name='ObjectId']",
message:
'Construct ObjectId with `new ObjectId()` instead of `ObjectId()`',
},
// Require `new` when mapping a list of ids to a list of ObjectId (For mongo + mongoose upgrade)
{
selector:
"CallExpression[callee.property.name='map'] Identifier[name='ObjectId']:first-child, CallExpression[callee.property.name='map'] MemberExpression[property.name='ObjectId']:first-child",
message:
"Don't map ObjectId directly. Use `id => new ObjectId(id)` instead",
},
],
'@typescript-eslint/no-floating-promises': 'error',
},
},
{
// Backend tests and scripts specific rules
files: ['**/test/**/*.*', '**/scripts/*.*'],
rules: {
'no-restricted-syntax': [
'error',
// Require `new` when constructing ObjectId (For mongo + mongoose upgrade)
{
selector:
"CallExpression[callee.name='ObjectId'], CallExpression[callee.property.name='ObjectId']",
message:
'Construct ObjectId with `new ObjectId()` instead of `ObjectId()`',
},
// Require `new` when mapping a list of ids to a list of ObjectId (For mongo + mongoose upgrade)
{
selector:
"CallExpression[callee.property.name='map'] Identifier[name='ObjectId']:first-child, CallExpression[callee.property.name='map'] MemberExpression[property.name='ObjectId']:first-child",
message:
"Don't map ObjectId directly. Use `id => new ObjectId(id)` instead",
},
// Catch incorrect usage of `await db.collection.find()`
{
selector:
"AwaitExpression > CallExpression > MemberExpression[property.name='find'][object.object.name='db']",
message:
'Mongo find returns a cursor not a promise, use `for await (const result of cursor)` or `.toArray()` instead.',
},
],
},
},
{
// Cypress specific rules
files: [
'cypress/**/*.{js,jsx,ts,tsx}',
'**/test/frontend/**/*.spec.{js,jsx,ts,tsx}',
],
extends: ['plugin:cypress/recommended'],
},
{
// Frontend specific rules
files: [
'**/frontend/js/**/*.{js,jsx,ts,tsx}',
'**/frontend/stories/**/*.{js,jsx,ts,tsx}',
'**/*.stories.{js,jsx,ts,tsx}',
'**/test/frontend/**/*.{js,jsx,ts,tsx}',
'**/test/frontend/components/**/*.spec.{js,jsx,ts,tsx}',
],
env: {
browser: true,
},
parserOptions: {
sourceType: 'module',
},
plugins: ['jsx-a11y'],
extends: [
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'standard-jsx',
'prettier',
],
globals: {
__webpack_public_path__: true,
$: true,
angular: true,
ga: true,
},
rules: {
// TODO: remove once https://github.com/standard/eslint-config-standard-react/issues/68 (support eslint@8) is fixed.
// START: inline standard-react rules
// "react/jsx-no-bind": ["error", {
// "allowArrowFunctions": true,
// "allowBind": false,
// "ignoreRefs": true
// },],
'react/no-did-update-set-state': 'error',
'react/no-unused-prop-types': 'error',
'react/prop-types': 'error',
// "react/react-in-jsx-scope": "error",
// END: inline standard-react rules
'react/no-unknown-property': [
'error',
{
ignore: ['dnd-container', 'dropdown-toggle'],
},
],
'react/jsx-no-target-blank': [
'error',
{
allowReferrer: true,
},
],
// Prevent usage of legacy string refs
'react/no-string-refs': 'error',
// Prevent curly braces around strings (as they're unnecessary)
'react/jsx-curly-brace-presence': [
'error',
{
props: 'never',
children: 'never',
},
],
// Don't import React for JSX; the JSX runtime is added by a Babel plugin
'react/react-in-jsx-scope': 'off',
'react/jsx-uses-react': 'off',
// Allow functions as JSX props
'react/jsx-no-bind': 'off', // TODO: fix occurrences and re-enable this
// Fix conflict between prettier & standard by overriding to prefer
// double quotes
'jsx-quotes': ['error', 'prefer-double'],
// Override weird behaviour of jsx-a11y label-has-for (says labels must be
// nested *and* have for/id attributes)
'jsx-a11y/label-has-for': [
'error',
{
required: {
some: ['nesting', 'id'],
},
},
],
// Require .jsx or .tsx file extension when using JSX
'react/jsx-filename-extension': [
'error',
{
extensions: ['.jsx', '.tsx'],
},
],
'no-restricted-syntax': [
'error',
// Begin: Make sure angular can withstand minification
{
selector:
"CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > :function[params.length > 0]",
message:
"Wrap the function in an array with the parameter names, to withstand minifcation. E.g. App.controller('MyController', ['param1', function(param1) {}]",
},
{
selector:
"CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrayExpression > ArrowFunctionExpression",
message:
'Use standard function syntax instead of arrow function syntax in angular components. E.g. function(param1) {}',
},
{
selector:
"CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrowFunctionExpression",
message:
'Use standard function syntax instead of arrow function syntax in angular components. E.g. function(param1) {}',
},
{
selector:
"CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrayExpression > :not(:function, Identifier):last-child",
message:
"Last element of the array must be a function. E.g ['param1', function(param1) {}]",
},
{
selector:
"CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrayExpression[elements.length=0]",
message:
"Array must not be empty. Add parameters and a function. E.g ['param1', function(param1) {}]",
},
// End: Make sure angular can withstand minification
// prohibit direct calls to methods of window.localStorage
{
selector:
"CallExpression[callee.object.object.name='window'][callee.object.property.name='localStorage']",
message:
'Modify location via customLocalStorage instead of calling window.localStorage methods directly',
},
],
},
},
{
// React component specific rules
//
files: [
'**/frontend/js/**/components/**/*.{js,jsx,ts,tsx}',
'**/frontend/js/**/hooks/**/*.{js,jsx,ts,tsx}',
],
rules: {
'@overleaf/no-unnecessary-trans': 'error',
'@overleaf/should-unescape-trans': 'error',
// https://astexplorer.net/
'no-restricted-syntax': [
'error',
// prohibit direct calls to methods of window.location
{
selector:
"CallExpression[callee.object.object.name='window'][callee.object.property.name='location']",
message:
'Modify location via useLocation instead of calling window.location methods directly',
},
// prohibit assignment to window.location
{
selector:
"AssignmentExpression[left.object.name='window'][left.property.name='location']",
message:
'Modify location via useLocation instead of calling window.location methods directly',
},
// prohibit assignment to window.location.href
{
selector:
"AssignmentExpression[left.object.object.name='window'][left.object.property.name='location'][left.property.name='href']",
message:
'Modify location via useLocation instead of calling window.location methods directly',
},
// prohibit using lookbehinds due to incidents with Safari simply crashing when the script is parsed
{
selector: 'Literal[regex.pattern=/\\(\\?<[!=]/]',
message: 'Lookbehind is not supported in older Safari versions.',
},
// prohibit direct calls to methods of window.localStorage
// NOTE: this rule is also defined for all frontend files, but those rules are overriden by the React component-specific config
{
selector:
"CallExpression[callee.object.object.name='window'][callee.object.property.name='localStorage']",
message:
'Modify location via customLocalStorage instead of calling window.localStorage methods directly',
},
],
},
},
// React + TypeScript-specific rules
{
files: ['**/*.tsx'],
rules: {
'react/prop-types': 'off',
'no-undef': 'off',
},
},
// TypeScript-specific rules
{
files: ['**/*.ts'],
rules: {
'no-undef': 'off',
},
},
{
files: ['scripts/ukamf/*.js'],
rules: {
// Do not allow importing of any dependencies unless specified in either
// - web/package.json
// - web/scripts/ukamf/package.json
'import/no-extraneous-dependencies': [
'error',
{ packageDir: ['.', 'scripts/ukamf'] },
],
},
},
{
files: ['scripts/learn/checkSanitize/*.js'],
rules: {
// The checkSanitize script is used in the dev-env only.
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: true,
packageDir: ['.', '../../'],
},
],
},
},
{
files: [
// Backend: Use @overleaf/logger
// Docs: https://manual.dev-overleaf.com/development/code/logging/#structured-logging
'**/app/**/*.{js,cjs,mjs}',
'app.js',
'modules/*/*.js',
// Frontend: Prefer debugConsole over bare console
// Docs: https://manual.dev-overleaf.com/development/code/logging/#frontend
'**/frontend/**/*.{js,jsx,ts,tsx}',
// Tests
'**/test/**/*.{js,cjs,mjs,jsx,ts,tsx}',
],
excludedFiles: [
// Allow console logs in scripts
'**/scripts/**/*.js',
// Allow console logs in stories
'**/stories/**/*.{js,jsx,ts,tsx}',
// Workers do not have access to the search params for enabling ?debug=true.
// self.location.url is the URL of the worker script.
'*.worker.{js,ts}',
],
rules: {
'no-console': 'error',
},
},
],
}