|
| 1 | +/** |
| 2 | + * Force all internal use of `captureException` to come from `@sentry/core` rather than `@sentry/browser`, |
| 3 | + * `@sentry/node`, or any wrapper SDK, in order to prevent accidental inclusion of manual-usage mechansism values. |
| 4 | + * |
| 5 | + * TODO (maybe): Doesn't catch unpacking of the module object (code like |
| 6 | + * |
| 7 | + * `import * as Sentry from '@sentry/xxx'; const { captureException } = Sentry; captureException(...);` |
| 8 | + * |
| 9 | + * ) because it's unlikely we'd do that and the rule would probably be more complicated than it's worth. (There are |
| 10 | + * probably other strange ways to call the wrong version of `captureException`, and this rule doesn't catch those, |
| 11 | + * either, but again, unlikely to come up in real life.) |
| 12 | + */ |
| 13 | + |
| 14 | +/** @type {import('eslint').Rule.RuleModule} */ |
| 15 | +module.exports = { |
| 16 | + meta: { |
| 17 | + type: 'problem', |
| 18 | + docs: { |
| 19 | + description: 'Enforce internal usage of `captureException` from `@sentry/core`', |
| 20 | + }, |
| 21 | + messages: { |
| 22 | + errorMessage: |
| 23 | + 'All internal uses of `captureException` should come from `@sentry/core`, not the browser or node SDKs (or any of their wrappers). (The browser and node versions of `captureException` have manual-capture `mechanism` data baked in, which is probably not what you want.)', |
| 24 | + }, |
| 25 | + }, |
| 26 | + |
| 27 | + create: function (context) { |
| 28 | + return { |
| 29 | + // This catches imports of the form `import { captureException } from '@sentry/xxx';` |
| 30 | + ImportDeclaration: function (node) { |
| 31 | + if ( |
| 32 | + node.specifiers.some( |
| 33 | + specifier => |
| 34 | + specifier.type === 'ImportSpecifier' && |
| 35 | + specifier.imported.type === 'Identifier' && |
| 36 | + specifier.imported.name === 'captureException', |
| 37 | + ) && |
| 38 | + node.source.value !== '@sentry/core' |
| 39 | + ) { |
| 40 | + context.report({ node, messageId: 'errorMessage' }); |
| 41 | + } |
| 42 | + }, |
| 43 | + |
| 44 | + // This catches uses like `import * as Sentry from '@sentry/xxx'; Sentry.captureException(...);` |
| 45 | + CallExpression: function (node) { |
| 46 | + if (node.callee.type === 'MemberExpression' && node.callee.property.name === 'captureException') { |
| 47 | + // NOTE: In all comments below, "the object" refers to the object (presumably a module) containing `captureException`. |
| 48 | + |
| 49 | + // This is the name of the object. IOW, it's the `Sentry` in `Sentry.captureException`. |
| 50 | + const objectName = node.callee.object.name; |
| 51 | + |
| 52 | + // All statements defining the object. (Not entirely clear how there there could be more than one, but |
| 53 | + // ¯\_(ツ)_/¯. Note: When we find a reference to the object, it may or may not be the reference in |
| 54 | + // `Sentry.captureException`, but we don't care, because we just want to use it to jump back to the original |
| 55 | + // definition.) |
| 56 | + const objectDefinitions = context |
| 57 | + .getScope() |
| 58 | + .references.find(reference => reference.identifier && reference.identifier.name === objectName) |
| 59 | + .resolved.defs; |
| 60 | + |
| 61 | + // Of the definitions, one which comes as part of an import, if any |
| 62 | + const namespaceImportDef = objectDefinitions.find(definition => definition.type === 'ImportBinding'); |
| 63 | + |
| 64 | + if ( |
| 65 | + namespaceImportDef && |
| 66 | + namespaceImportDef.parent.type === 'ImportDeclaration' && |
| 67 | + namespaceImportDef.parent.source.value !== '@sentry/core' |
| 68 | + ) { |
| 69 | + context.report({ node, messageId: 'errorMessage' }); |
| 70 | + } |
| 71 | + } |
| 72 | + }, |
| 73 | + }; |
| 74 | + }, |
| 75 | +}; |
0 commit comments