.eslintrc.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. const { readGlobalsFile } = require('./scripts/webpack-util');
  2. const FILES_INJECTED = [`src/injected/**/*.js`];
  3. const FILES_CONTENT = [
  4. 'src/injected/*.js',
  5. 'src/injected/content/**/*.js',
  6. ];
  7. const FILES_WEB = [`src/injected/web/**/*.js`];
  8. /* Note that `injected` uses several more `common` files indirectly, but we check just these
  9. * two automatically because they are trivial by design and must always pass the check */
  10. const FILES_SHARED = [
  11. 'src/common/browser.js',
  12. 'src/common/consts.js',
  13. ];
  14. const GLOBALS_COMMON = getGlobals('src/common/safe-globals.js');
  15. const GLOBALS_INJECTED = getGlobals(`src/injected/safe-globals-injected.js`);
  16. const GLOBALS_CONTENT = {
  17. INIT_FUNC_NAME: false,
  18. ...getGlobals(`src/injected/content/safe-globals-content.js`),
  19. ...GLOBALS_INJECTED,
  20. };
  21. const GLOBALS_WEB = {
  22. ...getGlobals(`src/injected/web/safe-globals-web.js`),
  23. ...GLOBALS_INJECTED,
  24. IS_FIREFOX: false, // passed as a parameter to VMInitInjection in webpack.conf.js
  25. };
  26. const INJECTED_RULES = {
  27. 'no-restricted-imports': ['error', {
  28. patterns: ['*/common', '*/common/*'],
  29. }],
  30. 'no-restricted-syntax': [
  31. 'error', {
  32. selector: 'ObjectExpression > ExperimentalSpreadProperty',
  33. message: 'Object spread adds a polyfill in injected* even if unused by it',
  34. }, {
  35. selector: 'OptionalCallExpression',
  36. message: 'Optional call uses .call(), which may be spoofed/broken in an unsafe environment',
  37. // TODO: write a Babel plugin to use safeCall for this.
  38. }, {
  39. selector: 'ArrayPattern',
  40. message: 'Destructuring via Symbol.iterator may be spoofed/broken in an unsafe environment',
  41. }, {
  42. selector: ':matches(ArrayExpression, CallExpression) > SpreadElement',
  43. message: 'Spreading via Symbol.iterator may be spoofed/broken in an unsafe environment',
  44. }, {
  45. selector: '[callee.object.name="Object"], MemberExpression[object.name="Object"]',
  46. message: 'Using potentially spoofed methods in an unsafe environment',
  47. // TODO: auto-generate the rule using GLOBALS
  48. }, {
  49. selector: `CallExpression[callee.name="defineProperty"]:not(${[
  50. '[arguments.2.properties.0.key.name="__proto__"]',
  51. ':has(CallExpression[callee.name="createNullObj"])'
  52. ].join(',')})`,
  53. message: 'Prototype of descriptor may be spoofed/broken in an unsafe environment',
  54. }
  55. ],
  56. };
  57. module.exports = {
  58. root: true,
  59. extends: [
  60. require.resolve('@gera2ld/plaid/eslint'),
  61. require.resolve('@gera2ld/plaid-common-vue/eslint/vue3-js'),
  62. ],
  63. plugins: ['jest'],
  64. overrides: [{
  65. // `browser` is a local variable since we remove the global `chrome` and `browser` in injected*
  66. // to prevent exposing them to userscripts with `@inject-into content`
  67. files: ['*'],
  68. excludedFiles: [...FILES_INJECTED, ...FILES_SHARED],
  69. globals: {
  70. browser: false,
  71. ...GLOBALS_COMMON,
  72. },
  73. }, {
  74. files: FILES_SHARED,
  75. globals: GLOBALS_COMMON,
  76. }, {
  77. files: FILES_WEB,
  78. globals: GLOBALS_WEB,
  79. }, {
  80. files: FILES_CONTENT,
  81. globals: GLOBALS_CONTENT,
  82. }, {
  83. files: FILES_INJECTED,
  84. excludedFiles: [...FILES_CONTENT, ...FILES_WEB],
  85. // intersection of globals in CONTENT and WEB
  86. globals: Object.keys(GLOBALS_CONTENT).reduce((res, key) => (
  87. Object.assign(res, key in GLOBALS_WEB && { [key]: false })
  88. ), {}),
  89. }, {
  90. files: [...FILES_INJECTED, ...FILES_SHARED],
  91. rules: INJECTED_RULES,
  92. }, {
  93. files: FILES_WEB,
  94. rules: {
  95. ...INJECTED_RULES,
  96. 'no-restricted-syntax': [
  97. ...INJECTED_RULES['no-restricted-syntax'],
  98. {
  99. selector: '[regex], NewExpression[callee.name="RegExp"]',
  100. message: 'RegExp internally depends on a *ton* of stuff that may be spoofed or broken',
  101. // https://262.ecma-international.org/12.0/#sec-regexpexec
  102. },
  103. ],
  104. },
  105. }, {
  106. // build scripts
  107. files: [
  108. '*.js',
  109. 'scripts/*.js',
  110. 'scripts/*.mjs',
  111. ],
  112. env: { node: true },
  113. rules: {
  114. 'global-require': 0,
  115. 'import/newline-after-import': 0,
  116. 'import/no-extraneous-dependencies': 0, // spits errors in github action
  117. 'import/extensions': 0,
  118. }
  119. }, {
  120. files: ['*.vue'],
  121. rules: {
  122. 'vue/multi-word-component-names': 0,
  123. },
  124. }, {
  125. files: ['test/**'],
  126. env: {
  127. 'jest/globals': true,
  128. },
  129. }],
  130. rules: {
  131. 'prettier/prettier': 'off',
  132. // 'import/extensions': ['error', 'ignorePackages', {
  133. // js: 'never',
  134. // vue: 'never',
  135. // }],
  136. // copied from airbnb-base, replaced 4 with 8
  137. 'object-curly-newline': ['error', {
  138. ObjectExpression: { minProperties: 8, multiline: true, consistent: true },
  139. ObjectPattern: { minProperties: 8, multiline: true, consistent: true },
  140. ImportDeclaration: { minProperties: 8, multiline: true, consistent: true },
  141. ExportDeclaration: { minProperties: 8, multiline: true, consistent: true },
  142. }],
  143. },
  144. };
  145. function getGlobals(filename) {
  146. const res = {};
  147. const { ast } = readGlobalsFile(filename, { ast: true });
  148. ast.program.body.forEach(body => {
  149. const { declarations } = body.declaration || body;
  150. if (!declarations) return;
  151. declarations.forEach(function processId({
  152. id: {
  153. left,
  154. properties,
  155. name = left && left.name,
  156. },
  157. }) {
  158. if (name) {
  159. // const NAME = whatever
  160. res[name] = false;
  161. } else if (properties) {
  162. // const { NAME1, prototype: { NAME2: ALIAS2 } } = whatever
  163. properties.forEach(({ value }) => processId({ id: value }));
  164. }
  165. });
  166. });
  167. return res;
  168. }