Repository: eslint-community/eslint-plugin-eslint-plugin (js)

Issue:
Rule request: don't use the in operator on nodes
Continuing conversation from here https://github.com/typescript-eslint/typescript-eslint/pull/5610#discussion_r1014918727: We (typescript-eslint) generally try not to use string `in` checking pattern on AST nodes, as it's a little imprecise. We normally check for specific types.

```diff
- if ('async' in node && node.async) {
+ if (isFunctionLike(node) && node.async) {
```

How about making this an ESLint rule?

cc @eliasm307 since we were chatting 🙂 


Pull request description:
feat: add no-property-in-node rule
Fixes #326.

Patch:
diff --git a/.eslint-doc-generatorrc.js b/.eslint-doc-generatorrc.js
index 3279eca..4710079 100644
--- a/.eslint-doc-generatorrc.js
+++ b/.eslint-doc-generatorrc.js
@@ -6,6 +6,7 @@ const prettier = require('prettier');
 module.exports = {
   ignoreConfig: [
     'all',
+    'all-type-checked',
     'rules',
     'rules-recommended',
     'tests',
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index e62078f..0000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-version: 2
-updates:
-  - package-ecosystem: github-actions
-    directory: /
-    schedule:
-      interval: daily
-
-  - package-ecosystem: npm
-    directory: /
-    schedule:
-      interval: weekly
diff --git a/README.md b/README.md
index 9a7b4b5..d3c7169 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,19 @@
-# eslint-plugin-eslint-plugin ![CI](https://github.com/eslint-community/eslint-plugin-eslint-plugin/workflows/CI/badge.svg) [![NPM version](https://img.shields.io/npm/v/eslint-plugin-eslint-plugin.svg?style=flat)](https://npmjs.org/package/eslint-plugin-eslint-plugin) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)
+# eslint-plugin-eslint-plugin ![CI](https://github.com/eslint-community/eslint-plugin-eslint-plugin/workflows/CI/badge.svg) [![NPM version](https://img.shields.io/npm/v/eslint-plugin-eslint-plugin.svg?style=flat)](https://npmjs.org/package/eslint-plugin-eslint-plugin) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) <!-- omit from toc -->
 
 An ESLint plugin for linting ESLint plugins. Rules written in CJS, ESM, and TypeScript are all supported.
 
 <!-- vscode-markdown-toc -->
 
-- [Installation](#Installation)
-- [Usage](#Usage)
-- [Rules](#Rules)
-- [Presets](#Presets)
-  - [Semantic versioning policy](#Semanticversioningpolicy)
-  - [Preset usage](#Presetusage)
+- [Installation](#installation)
+- [Usage](#usage)
+  - [**.eslintrc.json**](#eslintrcjson)
+  - [`eslint.config.js` (requires eslint\>=v8.23.0)](#eslintconfigjs-requires-eslintv8230)
+- [Rules](#rules)
+  - [Rules](#rules-1)
+  - [Tests](#tests)
+- [Presets](#presets)
+  - [Semantic versioning policy](#semantic-versioning-policy)
+  - [Preset usage](#preset-usage)
 
 <!-- vscode-markdown-toc-config
 	numbering=false
@@ -75,56 +79,59 @@ module.exports = [
 💼 [Configurations](https://github.com/eslint-community/eslint-plugin-eslint-plugin#presets) enabled in.\
 ✅ Set in the `recommended` [configuration](https://github.com/eslint-community/eslint-plugin-eslint-plugin#presets).\
 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
-💡 Manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
+💡 Manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).\
+💭 Requires type information.
 
 ### Rules
 
-| Name                                                                         | Description                                                                                | 💼  | 🔧  | 💡  |
-| :--------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------- | :-- | :-- | :-- |
-| [fixer-return](docs/rules/fixer-return.md)                                   | require fixer functions to return a fix                                                    | ✅  |     |     |
-| [meta-property-ordering](docs/rules/meta-property-ordering.md)               | enforce the order of meta properties                                                       |     | 🔧  |     |
-| [no-deprecated-context-methods](docs/rules/no-deprecated-context-methods.md) | disallow usage of deprecated methods on rule context objects                               | ✅  | 🔧  |     |
-| [no-deprecated-report-api](docs/rules/no-deprecated-report-api.md)           | disallow the version of `context.report()` with multiple arguments                         | ✅  | 🔧  |     |
-| [no-missing-message-ids](docs/rules/no-missing-message-ids.md)               | disallow `messageId`s that are missing from `meta.messages`                                | ✅  |     |     |
-| [no-missing-placeholders](docs/rules/no-missing-placeholders.md)             | disallow missing placeholders in rule report messages                                      | ✅  |     |     |
-| [no-unused-message-ids](docs/rules/no-unused-message-ids.md)                 | disallow unused `messageId`s in `meta.messages`                                            | ✅  |     |     |
-| [no-unused-placeholders](docs/rules/no-unused-placeholders.md)               | disallow unused placeholders in rule report messages                                       | ✅  |     |     |
-| [no-useless-token-range](docs/rules/no-useless-token-range.md)               | disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()` | ✅  | 🔧  |     |
-| [prefer-message-ids](docs/rules/prefer-message-ids.md)                       | require using `messageId` instead of `message` or `desc` to report rule violations         | ✅  |     |     |
-| [prefer-object-rule](docs/rules/prefer-object-rule.md)                       | disallow function-style rules                                                              | ✅  | 🔧  |     |
-| [prefer-placeholders](docs/rules/prefer-placeholders.md)                     | require using placeholders for dynamic report messages                                     |     |     |     |
-| [prefer-replace-text](docs/rules/prefer-replace-text.md)                     | require using `replaceText()` instead of `replaceTextRange()`                              |     |     |     |
-| [report-message-format](docs/rules/report-message-format.md)                 | enforce a consistent format for rule report messages                                       |     |     |     |
-| [require-meta-docs-description](docs/rules/require-meta-docs-description.md) | require rules to implement a `meta.docs.description` property with the correct format      |     |     |     |
-| [require-meta-docs-url](docs/rules/require-meta-docs-url.md)                 | require rules to implement a `meta.docs.url` property                                      |     | 🔧  |     |
-| [require-meta-fixable](docs/rules/require-meta-fixable.md)                   | require rules to implement a `meta.fixable` property                                       | ✅  |     |     |
-| [require-meta-has-suggestions](docs/rules/require-meta-has-suggestions.md)   | require suggestable rules to implement a `meta.hasSuggestions` property                    | ✅  | 🔧  |     |
-| [require-meta-schema](docs/rules/require-meta-schema.md)                     | require rules to implement a `meta.schema` property                                        | ✅  |     | 💡  |
-| [require-meta-type](docs/rules/require-meta-type.md)                         | require rules to implement a `meta.type` property                                          | ✅  |     |     |
+| Name                                                                         | Description                                                                                | 💼  | 🔧  | 💡  | 💭  |
+| :--------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------- | :-- | :-- | :-- | :-- |
+| [fixer-return](docs/rules/fixer-return.md)                                   | require fixer functions to return a fix                                                    | ✅  |     |     |     |
+| [meta-property-ordering](docs/rules/meta-property-ordering.md)               | enforce the order of meta properties                                                       |     | 🔧  |     |     |
+| [no-deprecated-context-methods](docs/rules/no-deprecated-context-methods.md) | disallow usage of deprecated methods on rule context objects                               | ✅  | 🔧  |     |     |
+| [no-deprecated-report-api](docs/rules/no-deprecated-report-api.md)           | disallow the version of `context.report()` with multiple arguments                         | ✅  | 🔧  |     |     |
+| [no-missing-message-ids](docs/rules/no-missing-message-ids.md)               | disallow `messageId`s that are missing from `meta.messages`                                | ✅  |     |     |     |
+| [no-missing-placeholders](docs/rules/no-missing-placeholders.md)             | disallow missing placeholders in rule report messages                                      | ✅  |     |     |     |
+| [no-property-in-node](docs/rules/no-property-in-node.md)                     | disallow using `in` to narrow node types instead of looking at properties                  |     |     |     | 💭  |
+| [no-unused-message-ids](docs/rules/no-unused-message-ids.md)                 | disallow unused `messageId`s in `meta.messages`                                            | ✅  |     |     |     |
+| [no-unused-placeholders](docs/rules/no-unused-placeholders.md)               | disallow unused placeholders in rule report messages                                       | ✅  |     |     |     |
+| [no-useless-token-range](docs/rules/no-useless-token-range.md)               | disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()` | ✅  | 🔧  |     |     |
+| [prefer-message-ids](docs/rules/prefer-message-ids.md)                       | require using `messageId` instead of `message` or `desc` to report rule violations         | ✅  |     |     |     |
+| [prefer-object-rule](docs/rules/prefer-object-rule.md)                       | disallow function-style rules                                                              | ✅  | 🔧  |     |     |
+| [prefer-placeholders](docs/rules/prefer-placeholders.md)                     | require using placeholders for dynamic report messages                                     |     |     |     |     |
+| [prefer-replace-text](docs/rules/prefer-replace-text.md)                     | require using `replaceText()` instead of `replaceTextRange()`                              |     |     |     |     |
+| [report-message-format](docs/rules/report-message-format.md)                 | enforce a consistent format for rule report messages                                       |     |     |     |     |
+| [require-meta-docs-description](docs/rules/require-meta-docs-description.md) | require rules to implement a `meta.docs.description` property with the correct format      |     |     |     |     |
+| [require-meta-docs-url](docs/rules/require-meta-docs-url.md)                 | require rules to implement a `meta.docs.url` property                                      |     | 🔧  |     |     |
+| [require-meta-fixable](docs/rules/require-meta-fixable.md)                   | require rules to implement a `meta.fixable` property                                       | ✅  |     |     |     |
+| [require-meta-has-suggestions](docs/rules/require-meta-has-suggestions.md)   | require suggestable rules to implement a `meta.hasSuggestions` property                    | ✅  | 🔧  |     |     |
+| [require-meta-schema](docs/rules/require-meta-schema.md)                     | require rules to implement a `meta.schema` property                                        | ✅  |     | 💡  |     |
+| [require-meta-type](docs/rules/require-meta-type.md)                         | require rules to implement a `meta.type` property                                          | ✅  |     |     |     |
 
 ### Tests
 
-| Name                                                                     | Description                                                                  | 💼  | 🔧  | 💡  |
-| :----------------------------------------------------------------------- | :--------------------------------------------------------------------------- | :-- | :-- | :-- |
-| [consistent-output](docs/rules/consistent-output.md)                     | enforce consistent use of `output` assertions in rule tests                  | ✅  |     |     |
-| [no-identical-tests](docs/rules/no-identical-tests.md)                   | disallow identical tests                                                     | ✅  | 🔧  |     |
-| [no-only-tests](docs/rules/no-only-tests.md)                             | disallow the test case property `only`                                       | ✅  |     | 💡  |
-| [prefer-output-null](docs/rules/prefer-output-null.md)                   | disallow invalid RuleTester test cases where the `output` matches the `code` | ✅  | 🔧  |     |
-| [test-case-property-ordering](docs/rules/test-case-property-ordering.md) | require the properties of a test case to be placed in a consistent order     |     | 🔧  |     |
-| [test-case-shorthand-strings](docs/rules/test-case-shorthand-strings.md) | enforce consistent usage of shorthand strings for test cases with no options |     | 🔧  |     |
+| Name                                                                     | Description                                                                  | 💼  | 🔧  | 💡  | 💭  |
+| :----------------------------------------------------------------------- | :--------------------------------------------------------------------------- | :-- | :-- | :-- | :-- |
+| [consistent-output](docs/rules/consistent-output.md)                     | enforce consistent use of `output` assertions in rule tests                  | ✅  |     |     |     |
+| [no-identical-tests](docs/rules/no-identical-tests.md)                   | disallow identical tests                                                     | ✅  | 🔧  |     |     |
+| [no-only-tests](docs/rules/no-only-tests.md)                             | disallow the test case property `only`                                       | ✅  |     | 💡  |     |
+| [prefer-output-null](docs/rules/prefer-output-null.md)                   | disallow invalid RuleTester test cases where the `output` matches the `code` | ✅  | 🔧  |     |     |
+| [test-case-property-ordering](docs/rules/test-case-property-ordering.md) | require the properties of a test case to be placed in a consistent order     |     | 🔧  |     |     |
+| [test-case-shorthand-strings](docs/rules/test-case-shorthand-strings.md) | enforce consistent usage of shorthand strings for test cases with no options |     | 🔧  |     |     |
 
 <!-- end auto-generated rules list -->
 
 ## <a name='Presets'></a>Presets
 
-|     | Name                | Description                                                               |
-| :-- | :------------------ | :------------------------------------------------------------------------ |
-| ✅  | `recommended`       | enables all recommended rules in this plugin                              |
-|     | `rules-recommended` | enables all recommended rules that are aimed at linting ESLint rule files |
-|     | `tests-recommended` | enables all recommended rules that are aimed at linting ESLint test files |
-|     | `all`               | enables all rules in this plugin                                          |
-|     | `rules`             | enables all rules that are aimed at linting ESLint rule files             |
-|     | `tests`             | enables all rules that are aimed at linting ESLint test files             |
+|     | Name                | Description                                                                  |
+| :-- | :------------------ | :--------------------------------------------------------------------------- |
+| ✅  | `recommended`       | enables all recommended rules in this plugin                                 |
+|     | `rules-recommended` | enables all recommended rules that are aimed at linting ESLint rule files    |
+|     | `tests-recommended` | enables all recommended rules that are aimed at linting ESLint test files    |
+|     | `all`               | enables all rules in this plugin, including those requiring type information |
+|     | `all-type-checked`  | enables all rules in this plugin, including those requiring type information |
+|     | `rules`             | enables all rules that are aimed at linting ESLint rule files                |
+|     | `tests`             | enables all rules that are aimed at linting ESLint test files                |
 
 ### <a name='Semanticversioningpolicy'></a>Semantic versioning policy
 
diff --git a/configs/all-type-checked.js b/configs/all-type-checked.js
new file mode 100644
index 0000000..e32d57d
--- /dev/null
+++ b/configs/all-type-checked.js
@@ -0,0 +1,8 @@
+'use strict';
+
+const mod = require('../lib/index.js');
+
+module.exports = {
+  plugins: { 'eslint-plugin': mod },
+  rules: mod.configs['all-type-checked'].rules,
+};
diff --git a/docs/rules/no-property-in-node.md b/docs/rules/no-property-in-node.md
new file mode 100644
index 0000000..22da26d
--- /dev/null
+++ b/docs/rules/no-property-in-node.md
@@ -0,0 +1,56 @@
+# Disallow using `in` to narrow node types instead of looking at properties (`eslint-plugin/no-property-in-node`)
+
+💭 This rule requires type information.
+
+<!-- end auto-generated rule header -->
+
+When working with a node of type `ESTree.Node` or `TSESTree.Node`, it can be tempting to use the `'in'` operator to narrow the node's type.
+`'in'` narrowing is susceptible to confusing behavior from quirks of ASTs, such as node properties sometimes being omitted from nodes and other times explicitly being set to `null` or `undefined`.
+
+Instead, checking a node's `type` property is generally considered preferable.
+
+## Rule Details
+
+Examples of **incorrect** code for this rule:
+
+```ts
+/* eslint eslint-plugin/no-property-in-node: error */
+
+/** @type {import('eslint').Rule.RuleModule} */
+module.exports = {
+  meta: {
+    /* ... */
+  },
+  create(context) {
+    return {
+      'ClassDeclaration, FunctionDeclaration'(node) {
+        if ('superClass' in node) {
+          console.log('This is a class declaration:', node);
+        }
+      },
+    };
+  },
+};
+```
+
+Examples of **correct** code for this rule:
+
+```ts
+/* eslint eslint-plugin/no-property-in-node: error */
+
+/** @type {import('eslint').Rule.RuleModule} */
+module.exports = {
+  meta: {
+    /* ... */
+  },
+  create(context) {
+    return {
+      'ClassDeclaration, FunctionDeclaration'(node) {
+        if (node.type === 'ClassDeclaration') {
+          console.log('This is a class declaration:', node);
+        }
+      },
+    };
+  },
+};
+```
diff --git a/lib/index.js b/lib/index.js
index 4ff4ef7..b09550a 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -15,7 +15,8 @@ const packageMetadata = require('../package');
 const PLUGIN_NAME = packageMetadata.name.replace(/^eslint-plugin-/, '');
 
 const configFilters = {
-  all: () => true,
+  all: (rule) => !rule.meta.docs.requiresTypeChecking,
+  'all-type-checked': () => true,
   recommended: (rule) => rule.meta.docs.recommended,
   rules: (rule) => rule.meta.docs.category === 'Rules',
   tests: (rule) => rule.meta.docs.category === 'Tests',
diff --git a/lib/rules/no-property-in-node.js b/lib/rules/no-property-in-node.js
new file mode 100644
index 0000000..f3e2e0c
--- /dev/null
+++ b/lib/rules/no-property-in-node.js
@@ -0,0 +1,86 @@
+'use strict';
+
+const typedNodeSourceFileTesters = [
+  /@types[/\\]estree[/\\]index\.d\.ts/,
+  /@typescript-eslint[/\\]types[/\\]dist[/\\]generated[/\\]ast-spec\.d\.ts/,
+];
+
+/**
+ * Given a TypeScript type, determines whether the type appears to be for a known
+ * AST type from the typings of @typescript-eslint/types or estree.
+ * We check based on two rough conditions:
+ * - The type has a 'kind' property (as AST node types all do)
+ * - The type is declared in one of those package's .d.ts types
+ *
+ * @example
+ * ```
+ * module.exports = {
+ *  create(context) {
+ *    BinaryExpression(node) {
+ *      const type = services.getTypeAtLocation(node.right);
+ *      //    ^^^^
+ *      // This variable's type will be TSESTree.BinaryExpression
+ *    }
+ *  }
+ * }
+ * ```
+ *
+ * @param {import('typescript').Type} type
+ * @returns Whether the type seems to include a known ESTree or TSESTree AST node.
+ */
+function isAstNodeType(type) {
+  return (type.types || [type])
+    .filter((typePart) => typePart.getProperty('type'))
+    .flatMap(
+      (typePart) => (typePart.symbol && typePart.symbol.declarations) || []
+    )
+    .some((declaration) => {
+      const fileName = declaration.getSourceFile().fileName;
+      return (
+        fileName &&
+        typedNodeSourceFileTesters.some((tester) => tester.test(fileName))
+      );
+    });
+}
+
+/** @type {import('eslint').Rule.RuleModule} */
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'disallow using `in` to narrow node types instead of looking at properties',
+      category: 'Rules',
+      recommended: false,
+      requiresTypeChecking: true,
+      url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-property-in-node.md',
+    },
+    schema: [],
+    messages: {
+      in: 'Prefer checking specific node properties instead of a broad `in`.',
+    },
+  },
+
+  create(context) {
+    return {
+      'BinaryExpression[operator=in]'(node) {
+        // TODO: Switch this to ESLintUtils.getParserServices with typescript-eslint@>=6
+        // https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/269
+        const services = (context.sourceCode || context).parserServices;
+        if (!services.program) {
+          throw new Error(
+            'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.'
+          );
+        }
+
+        const checker = services.program.getTypeChecker();
+        const tsNode = services.esTreeNodeToTSNodeMap.get(node.right);
+        const type = checker.getTypeAtLocation(tsNode);
+
+        if (isAstNodeType(type)) {
+          context.report({ messageId: 'in', node });
+        }
+      },
+    };
+  },
+};
diff --git a/package.json b/package.json
index 3b2f1b2..6dd21a8 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,10 @@
     "@eslint/eslintrc": "^2.0.2",
     "@eslint/js": "^8.37.0",
     "@release-it/conventional-changelog": "^4.3.0",
-    "@typescript-eslint/parser": "^5.36.2",
+    "@types/eslint": "^8.56.2",
+    "@types/estree": "^1.0.5",
+    "@typescript-eslint/parser": "^5.62.0",
+    "@typescript-eslint/utils": "^5.62.0",
     "chai": "^4.3.6",
     "dirty-chai": "^2.0.1",
     "eslint": "^8.23.0",
@@ -81,7 +84,7 @@
     "nyc": "^15.1.0",
     "prettier": "^2.7.1",
     "release-it": "^14.14.3",
-    "typescript": "^5.0.4"
+    "typescript": "5.1.3"
   },
   "peerDependencies": {
     "eslint": ">=7.0.0"


Test patch:
diff --git a/tests/lib/fixtures/estree.ts b/tests/lib/fixtures/estree.ts
new file mode 100644
index 0000000..e69de29
diff --git a/tests/lib/fixtures/file.ts b/tests/lib/fixtures/file.ts
new file mode 100644
index 0000000..e69de29
diff --git a/tests/lib/fixtures/tsconfig.json b/tests/lib/fixtures/tsconfig.json
new file mode 100644
index 0000000..0d505be
--- /dev/null
+++ b/tests/lib/fixtures/tsconfig.json
@@ -0,0 +1,5 @@
+{
+  "compilerOptions": {
+    "moduleResolution": "NodeNext"
+  }
+}
diff --git a/tests/lib/rules/no-property-in-node.js b/tests/lib/rules/no-property-in-node.js
new file mode 100644
index 0000000..39e4587
--- /dev/null
+++ b/tests/lib/rules/no-property-in-node.js
@@ -0,0 +1,165 @@
+'use strict';
+
+const RuleTester = require('eslint').RuleTester;
+const path = require('path');
+const rule = require('../../../lib/rules/no-property-in-node');
+
+const ruleTester = new RuleTester({
+  parser: require.resolve('@typescript-eslint/parser'),
+  parserOptions: {
+    project: './tsconfig.json',
+    tsconfigRootDir: path.join(__dirname, '../fixtures'),
+  },
+});
+
+ruleTester.run('no-property-in-node', rule, {
+  valid: [
+    `'a' in window;`,
+    `
+      declare const node: Node;
+      'a' in node;
+    `,
+    `
+      type Node = { unrelated: true; };
+      declare const node: Node;
+      'a' in node;
+    `,
+    `
+      interface Node {
+        unrelated: true;
+      };
+      declare const node: Node;
+      'a' in node;
+    `,
+    `
+      declare const node: UnresolvedType;
+      'a' in node;
+    `,
+    `
+      import * as ESTree from 'estree';
+      declare const loc: ESTree.SourceLocation;
+      'a' in loc;
+    `,
+    `
+      import * as ESTree from 'estree';
+      declare const node: ESTree.Node;
+      a.superClass;
+    `,
+    `
+      import * as ESTree from 'estree';
+      declare const node: ESTree.Node;
+      a.type;
+    `,
+    `
+      import * as ESTree from 'estree';
+      declare const node: ESTree.Node;
+      a.type === 'ClassDeclaration';
+    `,
+    `
+      import * as ESTree from 'estree';
+      declare const node: ESTree.ClassDeclaration | ESTree.FunctionDeclaration;
+      a.type === 'ClassDeclaration';
+    `,
+    `
+      import { TSESTree } from '@typescript-eslint/utils';
+      declare const node: TSESTree.Node;
+      node.superClass;
+    `,
+    `
+      import { TSESTree } from '@typescript-eslint/utils';
+      declare const node: TSESTree.Node;
+      node.type;
+    `,
+    `
+      import { TSESTree } from '@typescript-eslint/utils';
+      declare const node: TSESTree.ClassDeclaration | TSESTree.FunctionDeclaration;
+      node.type === 'ClassDeclaration';
+    `,
+    `
+      import * as eslint from 'eslint';
+      const listener: eslint.Rule.RuleListener = {
+        ClassDeclaration(node) {
+          node.type;
+        },
+      };
+    `,
+    `
+      import * as eslint from 'eslint';
+      const listener: eslint.Rule.RuleListener = {
+        'ClassDeclaration, FunctionDeclaration'(node) {
+          node.type === 'ClassDeclaration';
+        },
+      };
+    `,
+  ],
+  invalid: [
+    {
+      code: `
+        import { TSESTree } from '@typescript-eslint/utils';
+        declare const node: TSESTree.Node;
+        'a' in node;
+      `,
+      errors: [
+        {
+          column: 9,
+          line: 4,
+          endColumn: 20,
+          endLine: 4,
+          messageId: 'in',
+        },
+      ],
+    },
+    {
+      code: `
+        import { TSESTree } from '@typescript-eslint/utils';
+        type Other = { key: true };
+        declare const node: TSESTree.Node | Other;
+        'a' in node;
+      `,
+      errors: [
+        {
+          column: 9,
+          line: 5,
+          endColumn: 20,
+          endLine: 5,
+          messageId: 'in',
+        },
+      ],
+    },
+    {
+      code: `
+        import * as ESTree from 'estree';
+        declare const node: ESTree.Node;
+        'a' in node;
+      `,
+      errors: [
+        {
+          column: 9,
+          line: 4,
+          endColumn: 20,
+          endLine: 4,
+          messageId: 'in',
+        },
+      ],
+    },
+    {
+      code: `
+        import * as eslint from 'eslint';
+        const listener: eslint.Rule.RuleListener = {
+          ClassDeclaration(node) {
+            'a' in node;
+          },
+        };
+      `,
+      errors: [
+        {
+          column: 13,
+          line: 5,
+          endColumn: 24,
+          endLine: 5,
+          messageId: 'in',
+        },
+      ],
+    },
+  ],
+});