|
| 1 | +/* eslint-disable unicorn/filename-case */ |
| 2 | +const path = require('path') |
| 3 | + |
| 4 | +// Declare the local rules used by the Browser SDK |
| 5 | +// |
| 6 | +// See https://eslint.org/docs/developer-guide/working-with-rules for documentation on how to write |
| 7 | +// rules. |
| 8 | +// |
| 9 | +// You can use https://astexplorer.net/ to explore the parsed data structure of a code snippet. |
| 10 | +// Choose '@typescript-eslint/parser' as a parser to have the exact same structure as our ESLint |
| 11 | +// parser. |
| 12 | +module.exports = { |
| 13 | + 'disallow-side-effects': { |
| 14 | + meta: { |
| 15 | + docs: { |
| 16 | + description: |
| 17 | + 'Disallow potential side effects when evaluating modules, to ensure modules content are tree-shakable.', |
| 18 | + recommended: false, |
| 19 | + }, |
| 20 | + schema: [], |
| 21 | + }, |
| 22 | + create(context) { |
| 23 | + const filename = context.getFilename() |
| 24 | + if (pathsWithSideEffect.has(filename)) { |
| 25 | + return {} |
| 26 | + } |
| 27 | + return { |
| 28 | + Program(node) { |
| 29 | + reportPotentialSideEffect(context, node) |
| 30 | + }, |
| 31 | + } |
| 32 | + }, |
| 33 | + }, |
| 34 | +} |
| 35 | + |
| 36 | +const packagesRoot = `${__dirname}/packages` |
| 37 | + |
| 38 | +// Those modules are known to have side effects when evaluated |
| 39 | +const pathsWithSideEffect = new Set([ |
| 40 | + `${packagesRoot}/logs/src/boot/logs.entry.ts`, |
| 41 | + `${packagesRoot}/logs/src/index.ts`, |
| 42 | + `${packagesRoot}/rum-recorder/src/boot/recorder.entry.ts`, |
| 43 | + `${packagesRoot}/rum-recorder/src/index.ts`, |
| 44 | + `${packagesRoot}/rum/src/boot/rum.entry.ts`, |
| 45 | + `${packagesRoot}/rum/src/index.ts`, |
| 46 | +]) |
| 47 | + |
| 48 | +// Those packages are known to have no side effects when evaluated |
| 49 | +const packagesWithoutSideEffect = new Set(['@datadog/browser-core', '@datadog/browser-rum-core']) |
| 50 | + |
| 51 | +/** |
| 52 | + * Iterate over the given node and its children, and report any node that may have a side effect |
| 53 | + * when evaluated. |
| 54 | + * |
| 55 | + * @example |
| 56 | + * const foo = 1 // OK, this statement can't have any side effect |
| 57 | + * foo() // KO, we don't know what 'foo' does, report this |
| 58 | + * function bar() { // OK, a function declaration doesn't have side effects |
| 59 | + * foo() // OK, this statement won't be executed when evaluating the module code |
| 60 | + * } |
| 61 | + */ |
| 62 | +function reportPotentialSideEffect(context, node) { |
| 63 | + // This acts like an authorized list of syntax nodes to use directly in the body of a module. All |
| 64 | + // those nodes should not have a side effect when evaluated. |
| 65 | + // |
| 66 | + // This list is probably not complete, feel free to add more cases if you encounter an unhandled |
| 67 | + // node. |
| 68 | + switch (node.type) { |
| 69 | + case 'Program': |
| 70 | + node.body.forEach((child) => reportPotentialSideEffect(context, child)) |
| 71 | + return |
| 72 | + case 'TemplateLiteral': |
| 73 | + node.expressions.forEach((child) => reportPotentialSideEffect(context, child)) |
| 74 | + return |
| 75 | + case 'ExportNamedDeclaration': |
| 76 | + case 'ExportAllDeclaration': |
| 77 | + case 'ImportDeclaration': |
| 78 | + if (node.declaration) { |
| 79 | + reportPotentialSideEffect(context, node.declaration) |
| 80 | + } else if ( |
| 81 | + node.source && |
| 82 | + node.importKind !== 'type' && |
| 83 | + !isAllowedImport(context.getFilename(), node.source.value) |
| 84 | + ) { |
| 85 | + context.report({ |
| 86 | + node: node.source, |
| 87 | + message: 'This file cannot import modules with side-effects', |
| 88 | + }) |
| 89 | + } |
| 90 | + return |
| 91 | + case 'VariableDeclaration': |
| 92 | + node.declarations.forEach((child) => reportPotentialSideEffect(context, child)) |
| 93 | + return |
| 94 | + case 'VariableDeclarator': |
| 95 | + if (node.init) { |
| 96 | + reportPotentialSideEffect(context, node.init) |
| 97 | + } |
| 98 | + return |
| 99 | + case 'ArrayExpression': |
| 100 | + node.elements.forEach((child) => reportPotentialSideEffect(context, child)) |
| 101 | + return |
| 102 | + case 'UnaryExpression': |
| 103 | + reportPotentialSideEffect(context, node.argument) |
| 104 | + return |
| 105 | + case 'ObjectExpression': |
| 106 | + node.properties.forEach((child) => reportPotentialSideEffect(context, child)) |
| 107 | + return |
| 108 | + case 'SpreadElement': |
| 109 | + reportPotentialSideEffect(context, node.argument) |
| 110 | + return |
| 111 | + case 'Property': |
| 112 | + reportPotentialSideEffect(context, node.key) |
| 113 | + reportPotentialSideEffect(context, node.value) |
| 114 | + return |
| 115 | + case 'BinaryExpression': |
| 116 | + reportPotentialSideEffect(context, node.left) |
| 117 | + reportPotentialSideEffect(context, node.right) |
| 118 | + return |
| 119 | + case 'TSAsExpression': |
| 120 | + case 'ExpressionStatement': |
| 121 | + reportPotentialSideEffect(context, node.expression) |
| 122 | + return |
| 123 | + case 'MemberExpression': |
| 124 | + reportPotentialSideEffect(context, node.object) |
| 125 | + reportPotentialSideEffect(context, node.property) |
| 126 | + return |
| 127 | + case 'FunctionExpression': |
| 128 | + case 'ArrowFunctionExpression': |
| 129 | + case 'FunctionDeclaration': |
| 130 | + case 'ClassDeclaration': |
| 131 | + case 'TSEnumDeclaration': |
| 132 | + case 'TSInterfaceDeclaration': |
| 133 | + case 'TSTypeAliasDeclaration': |
| 134 | + case 'TSDeclareFunction': |
| 135 | + case 'Literal': |
| 136 | + case 'Identifier': |
| 137 | + return |
| 138 | + case 'CallExpression': |
| 139 | + if (isAllowedCallExpression(node)) { |
| 140 | + return |
| 141 | + } |
| 142 | + case 'NewExpression': |
| 143 | + if (isAllowedNewExpression(node)) { |
| 144 | + return |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + // If the node doesn't match any of the condition above, report it |
| 149 | + context.report({ |
| 150 | + node, |
| 151 | + message: `${node.type} can have side effects when the module is evaluated. \ |
| 152 | +Maybe move it in a function declaration?`, |
| 153 | + }) |
| 154 | +} |
| 155 | + |
| 156 | +/** |
| 157 | + * Make sure an 'import' statement does not pull a module or package with side effects. |
| 158 | + */ |
| 159 | +function isAllowedImport(basePath, source) { |
| 160 | + if (source.startsWith('.')) { |
| 161 | + const resolvedPath = `${path.resolve(path.dirname(basePath), source)}.ts` |
| 162 | + return !pathsWithSideEffect.has(resolvedPath) |
| 163 | + } |
| 164 | + return packagesWithoutSideEffect.has(source) |
| 165 | +} |
| 166 | + |
| 167 | +/* eslint-disable max-len */ |
| 168 | +/** |
| 169 | + * Authorize some call expressions. Feel free to add more exceptions here. Good candidates would |
| 170 | + * be functions that are known to be ECMAScript functions without side effects, that are likely to |
| 171 | + * be considered as pure functions by the bundler. |
| 172 | + * |
| 173 | + * You can experiment with Rollup tree-shaking strategy to ensure your function is known to be pure. |
| 174 | + * https://rollupjs.org/repl/?version=2.38.5&shareable=JTdCJTIybW9kdWxlcyUyMiUzQSU1QiU3QiUyMm5hbWUlMjIlM0ElMjJtYWluLmpzJTIyJTJDJTIyY29kZSUyMiUzQSUyMiUyRiUyRiUyMFB1cmUlMjBmdW5jdGlvbnMlNUNubmV3JTIwV2Vha01hcCgpJTVDbk9iamVjdC5rZXlzKCklNUNuJTVDbiUyRiUyRiUyMFNpZGUlMjBlZmZlY3QlMjBmdW5jdGlvbnMlNUNuZm9vKCklMjAlMkYlMkYlMjB1bmtub3duJTIwZnVuY3Rpb25zJTIwYXJlJTIwY29uc2lkZXJlZCUyMHRvJTIwaGF2ZSUyMHNpZGUlMjBlZmZlY3RzJTVDbmFsZXJ0KCdhYWEnKSU1Q25uZXclMjBNdXRhdGlvbk9ic2VydmVyKCgpJTIwJTNEJTNFJTIwJTdCJTdEKSUyMiUyQyUyMmlzRW50cnklMjIlM0F0cnVlJTdEJTVEJTJDJTIyb3B0aW9ucyUyMiUzQSU3QiUyMmZvcm1hdCUyMiUzQSUyMmVzJTIyJTJDJTIybmFtZSUyMiUzQSUyMm15QnVuZGxlJTIyJTJDJTIyYW1kJTIyJTNBJTdCJTIyaWQlMjIlM0ElMjIlMjIlN0QlMkMlMjJnbG9iYWxzJTIyJTNBJTdCJTdEJTdEJTJDJTIyZXhhbXBsZSUyMiUzQW51bGwlN0Q= |
| 175 | + * |
| 176 | + * Webpack is not as smart as Rollup, and it usually treat all call expressions as impure, but it |
| 177 | + * could be fine to allow it nonetheless at it pulls very little code. |
| 178 | + */ |
| 179 | +/* eslint-enable max-len */ |
| 180 | +function isAllowedCallExpression({ callee }) { |
| 181 | + // Allow "Object.keys()" |
| 182 | + if (callee.type === 'MemberExpression' && callee.object.name === 'Object' && callee.property.name === 'keys') { |
| 183 | + return true |
| 184 | + } |
| 185 | + |
| 186 | + return false |
| 187 | +} |
| 188 | + |
| 189 | +/* eslint-disable max-len */ |
| 190 | +/** |
| 191 | + * Authorize some 'new' expressions. Feel free to add more exceptions here. Good candidates would |
| 192 | + * be functions that are known to be ECMAScript functions without side effects, that are likely to |
| 193 | + * be considered as pure functions by the bundler. |
| 194 | + * |
| 195 | + * You can experiment with Rollup tree-shaking strategy to ensure your function is known to be pure. |
| 196 | + * https://rollupjs.org/repl/?version=2.38.5&shareable=JTdCJTIybW9kdWxlcyUyMiUzQSU1QiU3QiUyMm5hbWUlMjIlM0ElMjJtYWluLmpzJTIyJTJDJTIyY29kZSUyMiUzQSUyMiUyRiUyRiUyMFB1cmUlMjBmdW5jdGlvbnMlNUNubmV3JTIwV2Vha01hcCgpJTVDbk9iamVjdC5rZXlzKCklNUNuJTVDbiUyRiUyRiUyMFNpZGUlMjBlZmZlY3QlMjBmdW5jdGlvbnMlNUNuZm9vKCklMjAlMkYlMkYlMjB1bmtub3duJTIwZnVuY3Rpb25zJTIwYXJlJTIwY29uc2lkZXJlZCUyMHRvJTIwaGF2ZSUyMHNpZGUlMjBlZmZlY3RzJTVDbmFsZXJ0KCdhYWEnKSU1Q25uZXclMjBNdXRhdGlvbk9ic2VydmVyKCgpJTIwJTNEJTNFJTIwJTdCJTdEKSUyMiUyQyUyMmlzRW50cnklMjIlM0F0cnVlJTdEJTVEJTJDJTIyb3B0aW9ucyUyMiUzQSU3QiUyMmZvcm1hdCUyMiUzQSUyMmVzJTIyJTJDJTIybmFtZSUyMiUzQSUyMm15QnVuZGxlJTIyJTJDJTIyYW1kJTIyJTNBJTdCJTIyaWQlMjIlM0ElMjIlMjIlN0QlMkMlMjJnbG9iYWxzJTIyJTNBJTdCJTdEJTdEJTJDJTIyZXhhbXBsZSUyMiUzQW51bGwlN0Q= |
| 197 | + * |
| 198 | + * Webpack is not as smart as Rollup, and it usually treat all 'new' expressions as impure, but it |
| 199 | + * could be fine to allow it nonetheless at it pulls very little code. |
| 200 | + */ |
| 201 | +/* eslint-enable max-len */ |
| 202 | +function isAllowedNewExpression({ callee }) { |
| 203 | + // Allow "new WeakMap()" |
| 204 | + if (callee.name === 'WeakMap') { |
| 205 | + return true |
| 206 | + } |
| 207 | + |
| 208 | + return false |
| 209 | +} |
0 commit comments