You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何检查导入文件中类的Decorator?NestJS ESLint规则适配

解决NestJS自定义ESLint规则无法检查外部导入类的@Injectable装饰器问题

问题背景

我需要确保NestJS模块的@Module装饰器的providers数组中配置的类都带有@Injectable装饰器。自己编写的自定义ESLint规则在检查当前文件声明的类时正常工作,但对从外部文件导入的类失效,请问该如何处理?

现有实现代码(仅能处理当前文件类):

module.exports = {
  'injectable-providers': {
    create: function (context) {
      return {
        ClassDeclaration: function (node) {
          const moduleDecorator = nestModule.getModuleDecorator(node);
          if (!moduleDecorator) {
            return;
          }
          const properties = moduleDecorator.expression.arguments[0].properties;
          if (!properties) {
            return;
          }

          const providersProperty = properties.find(
            (property) => property.key.name === 'providers'
          );
          const providers = providersProperty?.value.elements ?? [];

          providers.forEach((provider) => {
            const providerNode = context.getScope().upper.set.get(provider.name)
              ?.defs[0]?.node;
            if (providerNode && !nestInjectable.isInjectable(providerNode)) {
              context.report({
                message: 'Provider is not `@Injectable`',
                node: provider,
              });
            }
          });
        },
      };
    },
  },
};

const nestModule = {
  getModuleDecorator: (node) =>
    node?.decorators?.find(
      (decorator) => decorator.expression.callee.name === 'Module'
    ),
};

const nestInjectable = {
  isInjectable: (node) => {
    const isClass = node?.type === 'ClassDeclaration';
    const hasInjectableDecorator = (node?.decorators ?? []).some(
      (decorator) => decorator.expression.callee.name === 'Injectable'
    );
    return node && isClass && hasInjectableDecorator;
  },
};

问题原因

现有代码仅通过当前文件的作用域查找类定义,但外部导入的类的AST节点不在当前文件的作用域内,因此无法获取其装饰器信息。

解决方案

要检查外部导入类的装饰器,必须借助TypeScript的类型检查器(TypeChecker)——ESLint的AST仅包含当前文件内容,外部文件的类信息需要通过TypeChecker解析类型元数据来获取。

修改后的规则代码

const { ESLintUtils } = require('@typescript-eslint/utils');

module.exports = {
  'injectable-providers': {
    create: function (context) {
      // 获取TypeScript类型检查器实例
      const typeChecker = ESLintUtils.getParserServices(context).program.getTypeChecker();

      return {
        ClassDeclaration: function (node) {
          const moduleDecorator = nestModule.getModuleDecorator(node);
          if (!moduleDecorator) return;

          const moduleOptions = moduleDecorator.expression.arguments[0];
          if (!moduleOptions || moduleOptions.type !== 'ObjectExpression') return;

          const providersProperty = moduleOptions.properties.find(
            prop => prop.key?.name === 'providers'
          );
          if (!providersProperty || providersProperty.value.type !== 'ArrayExpression') return;

          const providers = providersProperty.value.elements;
          providers.forEach(provider => {
            // 仅处理标识符类型的provider(排除工厂函数、直接值等情况)
            if (provider.type !== 'Identifier') return;

            // 获取provider对应的符号(关联到外部文件的类声明)
            const symbol = typeChecker.getSymbolAtLocation(provider);
            if (!symbol) return;

            // 从符号中提取外部文件的类声明节点
            const classDeclaration = symbol.declarations?.find(
              decl => decl.type === 'ClassDeclaration'
            );
            if (!classDeclaration) return;

            // 检查该类是否带有@Injectable装饰器
            if (!nestInjectable.isInjectable(classDeclaration)) {
              context.report({
                message: 'Provider 必须添加 `@Injectable` 装饰器',
                node: provider,
              });
            }
          });
        },
      };
    },
  },
};

const nestModule = {
  getModuleDecorator: (node) =>
    node?.decorators?.find(
      (decorator) => 
        decorator.expression.type === 'CallExpression' && 
        decorator.expression.callee.name === 'Module'
    ),
};

const nestInjectable = {
  isInjectable: (node) => {
    if (!node || node.type !== 'ClassDeclaration') return false;
    return node.decorators?.some(
      decorator => 
        decorator.expression.type === 'CallExpression' && 
        decorator.expression.callee.name === 'Injectable'
    );
  },
};

关键修改说明

  1. 引入TypeScript ESLint工具:通过@typescript-eslint/utils获取TypeChecker,用于解析外部类的类型信息
  2. 关联外部类声明:通过typeChecker.getSymbolAtLocation获取导入标识符对应的符号,进而找到外部文件中的类声明节点
  3. 增强AST节点判断:添加了对装饰器节点类型的判断,避免因AST结构不匹配导致的误判

注意事项

  • 确保ESLint配置中使用@typescript-eslint/parser作为解析器,并正确配置tsconfigRootDir指向项目的TypeScript配置文件
  • 规则依赖TypeScript类型信息,运行ESLint时需要保证项目已正确编译或TypeScript配置有效

内容的提问来源于stack exchange,提问作者Ivan Rubinson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 19:43:13