@longzai-intelligence-oxlint/nestjs-analyzer-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # @longzai-intelligence-oxlint/nestjs-analyzer-plugin
2
+
3
+ ## 意图
4
+
5
+ 对 NestJS 应用进行深度静态分析,从安全、性能、架构与可维护性多维度拦截潜在缺陷。
6
+
7
+ ## 定位
8
+
9
+ Oxlint 生态的 NestJS 深度分析插件,规则数量在生态内位居前列,是 NestJS 项目的核心治理载体。
10
+
11
+ ## 职能
12
+
13
+ 提供共二十五条规则,覆盖:禁用 console.log、禁用直接 process.env、缺失 Injectable、生命周期接口缺失、路由重复、缺失异常过滤器、禁用硬编码密钥、SQL 注入、原生 SQL、缺失 ValidationPipe、缺失 class-validator、禁用同步 fs、禁用阻塞式 crypto、缺失缓存、N+1 查询、上帝 Controller、上帝 Service、循环依赖、DI 绕过、硬编码依赖、缺失 Swagger 装饰器、Entity 作为响应、HTTP 状态不一致、通用异常、推荐分页等。
14
+
15
+ ## 实现情况
16
+
17
+ 二十五条规则均已实现并按规则目录组织,统一注册为 `@longzai-intelligence/nestjs-analyzer`;规则集合稳定且覆盖面广,配套 recommended 配置入口。
@@ -0,0 +1,12 @@
1
+ import { Rule } from "@oxlint/plugins";
2
+ //#region src/index.d.ts
3
+ /**
4
+ * nestjs-analyzer 规则集合
5
+ */
6
+ declare const nestjsAnalyzerRules: Record<string, Rule>;
7
+ /**
8
+ * nestjs-analyzer 插件
9
+ */
10
+ declare const nestjsAnalyzerPlugin: import("@oxlint/plugins").Plugin;
11
+ //#endregion
12
+ export { nestjsAnalyzerPlugin, nestjsAnalyzerRules };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{defineRule as e,eslintCompatPlugin as t}from"@oxlint/plugins";function n(e){return typeof e!=`object`||!e?!1:`name`in e}function r(e){return typeof e!=`object`||!e?!1:`value`in e}function i(e){return typeof e!=`object`||!e?!1:`id`in e}function a(e){return typeof e!=`object`||!e?!1:`type`in e}function o(e){return typeof e!=`object`||!e?!1:`implements`in e}function s(e){return!(typeof e!=`object`||!e||!(`body`in e))}function c(e){return n(e)?e.name:null}function l(e){return r(e)&&typeof e.value==`string`?e.value:null}function u(e){let t=e.callee;if(t.type===`Identifier`)return c(t);if(t.type===`MemberExpression`&&t.property.type===`Identifier`){let e=c(t.property);if(e===null)return null;if(t.object.type===`Identifier`){let n=c(t.object);return n===null?null:`${n}.${e}`}if(t.object.type===`MemberExpression`&&t.object.property.type===`Identifier`&&t.object.object.type===`Identifier`){let n=c(t.object.object),r=c(t.object.property);return n===null||r===null?null:`${n}.${r}.${e}`}}return null}function d(e){return e.callee.type===`Identifier`?c(e.callee):null}function f(e){if(!i(e))return null;let t=e.id;return t&&typeof t==`object`&&a(t)&&t.type===`Identifier`&&n(t)?t.name:null}function p(e){return e.key.type===`Identifier`?c(e.key):e.key.type===`Literal`?l(e.key):e.key.type===`PrivateIdentifier`?c(e.key):null}function m(e){return[`Service`,`Repository`,`Guard`,`Interceptor`,`Pipe`,`Filter`,`Middleware`,`Gateway`,`Resolver`,`Strategy`,`Factory`,`Handler`,`Subscriber`,`Listener`,`Processor`,`Consumer`,`Adapter`].some(t=>e.endsWith(t))}function ee(e){let t=[];if(!o(e))return t;let n=e.implements;if(!Array.isArray(n))return t;for(let e of n){if(!e||typeof e!=`object`)continue;let n=e;if(`expression`in n&&n.expression&&typeof n.expression==`object`){let e=n.expression;a(e)&&e.type===`Identifier`&&`name`in e&&t.push(String(e.name))}}return t}function h(e){return s(e)?e.body:null}function g(e){return typeof e==`object`&&!!e&&`parent`in e}function _(e){return typeof e==`object`&&!!e&&`value`in e}function v(e){return typeof e==`object`&&!!e&&`expression`in e}function y(e){return typeof e==`object`&&!!e&&`callee`in e}function te(e){return typeof e==`object`&&!!e&&`object`in e}function ne(e){return typeof e==`object`&&!!e&&`property`in e}function b(e){return typeof e==`object`&&!!e&&`params`in e}function re(e){return typeof e==`object`&&!!e&&`static`in e}function x(e){return typeof e==`object`&&!!e&&`key`in e}function ie(e){return typeof e==`object`&&!!e&&`init`in e}function S(e){return g(e)?e.parent:null}function C(e){let t=e.replace(/\\/g,`/`);return!!(t.includes(`.spec.`)||t.includes(`.test.`)||t.includes(`/cypress/`)||t.includes(`/__tests__/`)||t.includes(`/test/`)||t.includes(`/tests/`))}function w(e){let t=e.replace(/\\/g,`/`);return!!(t.includes(`/migrations/`)||t.includes(`/migration/`))}function T(e){let t=e.replace(/\\/g,`/`);return/[/\\]cli[/\\]/.test(t)||/[/\\]bin[/\\]/.test(t)||/[/\\]scripts[/\\]/.test(t)||t.startsWith(`cli/`)||t.startsWith(`bin/`)||t.startsWith(`scripts/`)}function ae(e){return e.includes(`/bootstrap/`)||e.startsWith(`bootstrap/`)}function oe(e){return e.startsWith(`setUp`)||e.startsWith(`setup`)}function se(e,t){return e.includes(`.instrumentation.`)||e.endsWith(`/instrumentation.ts`)||e.endsWith(`/instrumentation.js`)||t===`instrumentation.ts`||t===`instrumentation.js`||e.includes(`/instrumentation/`)}function ce(e){return e.includes(`run-migrations`)||e.includes(`run-migration`)||e.includes(`migrate.ts`)||e.includes(`migrate.js`)}function le(e,t){return!!(e.includes(`data-source`)||e.includes(`datasource`)||e.includes(`ormconfig`)||t.includes(`Datasource`)||t.includes(`DataSource`))}function ue(e){return e.includes(`typePicker`)||e.includes(`type-picker`)||e.includes(`columnType`)||e.includes(`column-type`)}function de(e){return e.includes(`loadOrm`)||e.includes(`orm-config`)||e.includes(`OrmConfig`)}function fe(e){return e.includes(`ClientFactory`)||e.includes(`clientFactory`)||e.includes(`PrismaFactory`)||e.includes(`prismaFactory`)}function pe(e,t){return t.includes(`Migrator`)||t.includes(`migrator`)||e.includes(`/migration/`)||e.includes(`/migrator/`)}function me(e,t){return t===`seed.ts`||t===`seed.js`||e.includes(`/seeds/`)||e.includes(`/seeders/`)}function E(e){let t=e.replace(/\\/g,`/`),n=t.split(`/`).pop()||``;return!!(ae(t)||oe(n)||se(t,n)||ce(t)||le(t,n)||ue(n)||de(n)||fe(n)||pe(t,n)||me(t,n))}function D(e){let t=e.replace(/\\/g,`/`);return t.includes(`/libs/`)||t.startsWith(`libs/`)}function he(e){let t=e.replace(/\\/g,`/`);return t.includes(`/mcp/`)||t.startsWith(`mcp/`)}function ge(e){let t=e.replace(/\\/g,`/`).split(`/`).pop()||``;return t.includes(`loadConfig`)||t.includes(`load-config`)||t.includes(`ormconfig`)||t.includes(`OrmConfig`)||t===`data-source.ts`||t===`data-source.js`||t.includes(`DataSource`)||t.includes(`datasource`)}function _e(e){let t=e.replace(/\\/g,`/`),n=t.split(`/`).pop()||``;return n===`seed.ts`||n===`seed.js`||n===`seeds.ts`||n===`seeds.js`||t.includes(`/seeds/`)||t.includes(`/seeders/`)||t.includes(`/prisma/seed`)}const ve=[/[/\\]DataFactories[/\\]/,/[/\\]Factories[/\\]/,/[/\\]factories[/\\]/,/[/\\]Strategies[/\\]/,/[/\\]strategies[/\\]/];function ye(e){return ve.some(t=>t.test(e))}function be(e){let t=e.replace(/\\/g,`/`).split(`/`).pop()||``;return t.includes(`Factory.ts`)||t.includes(`Factory.js`)||t.includes(`factory.ts`)||t.includes(`factory.js`)||t.includes(`ClientFactory`)||t.includes(`clientFactory`)}const xe=new Set([`console.log`,`console.warn`,`console.error`,`console.info`,`console.debug`,`console.trace`,`console.dir`,`console.table`]),Se=e({meta:{type:`suggestion`,docs:{description:`禁止直接使用 console 方法,应使用 NestJS Logger 服务`}},createOnce(e){let t=e.filename,n=t.split(`/`).pop()||``;if(C(t)||w(t)||T(t)||n.startsWith(`Mock`)||n.startsWith(`mock`)||E(t))return{};let r=e.sourceCode.text;return r.includes(`eslint-disable no-console`)||r.includes(`eslint-disable-next-line no-console`)||r.includes(`@Command(`)||r.includes(`@SubCommand(`)?{}:{CallExpression(t){if(t.type!==`CallExpression`)return;let n=u(t);n&&xe.has(n)&&e.report({node:t,message:`检测到 '${n}' — 请使用 NestJS Logger 服务代替`})}}}}),Ce=new Set([`PWD`,`HOME`,`USER`,`PATH`,`SHELL`,`TERM`,`LANG`,`LC_ALL`,`TZ`,`HOSTNAME`,`TMPDIR`]),we=new Set([`GIT_VERSION`,`GIT_COMMIT`,`GIT_BRANCH`,`GIT_TAG`,`BUILD_ID`,`BUILD_NUMBER`,`BUILD_VERSION`,`CI_COMMIT_SHA`,`CI_BUILD_ID`,`GITHUB_SHA`,`GITHUB_REF`]),Te=new Set([`USES_MIGRATION_CONTAINER`,`MIGRATION_CONTAINER`,`IS_MIGRATION_CONTAINER`,`RUN_MIGRATIONS`,`SKIP_MIGRATIONS`]),Ee=new Set([`=`,`+=`,`-=`,`*=`,`/=`,`%=`,`**=`,`<<=`,`>>=`,`>>>=`,`|=`,`^=`,`&=`,`||=`,`&&=`,`??=`]);function De(e){let t=e;for(;t&&typeof t==`object`&&g(t);){let e=t.parent;if(!e)break;if(a(e)&&e.type===`Decorator`&&v(e)&&typeof e.expression==`object`&&e.expression!==null){let t=e.expression;if(a(t)&&t.type===`CallExpression`&&y(t)&&typeof t.callee==`object`){let e=t.callee;if(e&&a(e)&&e.type===`Identifier`&&n(e)&&(e.name===`Module`||e.name===`Global`))return!0}}t=e}return!1}function Oe(e){let t=e;for(;t&&typeof t==`object`&&g(t);){let e=t.parent;if(!e)break;if(a(e)&&e.type===`CallExpression`&&y(e)&&typeof e.callee==`object`&&e.callee!==null){let t=e.callee;if(a(t)&&t.type===`MemberExpression`&&te(t)&&ne(t)&&typeof t.object==`object`&&typeof t.property==`object`){let e=t.object,r=t.property;if(e&&a(e)&&e.type===`Identifier`&&n(e)&&e.name===`ConfigModule`&&r&&a(r)&&r.type===`Identifier`&&n(r)&&(r.name===`forRoot`||r.name===`forRootAsync`))return!0}}t=e}return!1}function ke(e){let t=e;for(;t&&typeof t==`object`&&g(t);){let e=t.parent;if(!e)break;if(a(e)&&(e.type===`AssignmentExpression`&&`left`in e&&e.left===t&&`operator`in e&&typeof e.operator==`string`&&Ee.has(e.operator)||e.type===`AssignmentPattern`&&`left`in e&&e.left===t))return!0;t=e}return!1}function Ae(e){return e.includes(`main.ts`)||e.includes(`.config.`)||e.includes(`/config/`)}function je(e){return e.includes(`.module.`)||e.endsWith(`Module.ts`)||e.endsWith(`Module.js`)}function Me(e){if(C(e))return!0;let t=e.replace(/\\/g,`/`);return!!(Ae(t)||je(t)||D(e)||w(e)||T(e)||E(e))}function Ne(e,t){if(e.type!==`MemberExpression`||e.object.type!==`MemberExpression`)return;let n=e.object;if(n.object.type!==`Identifier`||n.object.name!==`process`||n.property.type!==`Identifier`||n.property.name!==`env`||ke(e)||De(e)||Oe(e))return;if(`computed`in e&&e.computed===!0){t.report({node:e,message:`直接访问 'process.env[...]' — 请使用 ConfigService.get() 代替`});return}let r=e.property.type===`Identifier`?e.property.name:`<dynamic>`;Ce.has(r)||we.has(r)||Te.has(r)||t.report({node:e,message:`直接访问 'process.env.${r}' — 请使用 ConfigService.get() 代替`})}const Pe=e({meta:{type:`suggestion`,docs:{description:`禁止直接访问 process.env,应使用 ConfigService.get()`}},createOnce(e){return Me(e.filename)?{}:{MemberExpression(t){Ne(t,e)}}}}),Fe=new Set(`Error.TypeError.RangeError.SyntaxError.ReferenceError.Date.Map.Set.WeakMap.WeakSet.Promise.Proxy.RegExp.Array.Object.URL.URLSearchParams.Headers.Request.Response.FormData.Buffer.EventEmitter.Subject.BehaviorSubject.ReplaySubject.Observable.ValidationPipe.ParseIntPipe.ParseBoolPipe.ParseArrayPipe.ParseUUIDPipe.ParseFloatPipe.DefaultValuePipe.ParseEnumPipe.HttpException.BadRequestException.UnauthorizedException.ForbiddenException.NotFoundException.ConflictException.InternalServerErrorException.NotImplementedException.BadGatewayException.ServiceUnavailableException.GatewayTimeoutException.PrismaClient`.split(`.`)),O=new Set([`Get`,`Post`,`Put`,`Delete`,`Patch`,`All`,`Head`,`Options`]),Ie=new Set([`Injectable`,`Controller`,`Module`,`Resolver`,`Catch`,`EventsHandler`,`CommandHandler`,`QueryHandler`,`Saga`,`WebSocketGateway`,`Processor`]);function Le(e){return typeof e!=`object`||!e?!1:`value`in e}function k(e){return`decorators`in e}function A(e){let t=e.expression;return t.type===`Identifier`?t.name:t.type===`CallExpression`&&t.callee.type===`Identifier`?t.callee.name:t.type===`CallExpression`&&t.callee.type===`MemberExpression`&&t.callee.property.type===`Identifier`?t.callee.property.name:null}function j(e){return e.expression.type===`CallExpression`?e.expression.arguments.filter(e=>e.type!==`SpreadElement`):[]}function Re(e){let t=j(e);if(t.length===0)return null;let n=t[0];if(!n)return null;if(n.type===`Literal`&&Le(n)){let e=n.value;if(typeof e==`string`)return e}return null}function M(e){if(!k(e)||!Array.isArray(e.decorators))return[];let t=e.decorators??[],n=[];for(let e of t){let t=A(e);t&&n.push({name:t,arguments:j(e)})}return n}function N(e,t){return!k(e)||!Array.isArray(e.decorators)?!1:(e.decorators??[]).some(e=>A(e)===t)}function P(e,t){return!k(e)||!Array.isArray(e.decorators)?null:(e.decorators??[]).find(e=>A(e)===t)??null}const ze=Array.from(Ie);function Be(e){return e.type===`ClassDeclaration`||e.type===`ClassExpression`}function Ve(e){return Be(e)?e.abstract===!0:!1}function He(e){return e.type===`MethodDefinition`}function Ue(e){let t=(h(e)?.body??[]).filter(He);return t.length!==0&&t.every(e=>e.static===!0)}function We(e){let t=h(e)?.body??[];for(let e of t)if(He(e)&&e.kind===`constructor`)return e.value.params.length>0;return!1}function Ge(e){return Be(e)?Array.isArray(e.implements)&&e.implements.length>0:!1}const Ke=e({meta:{type:`suggestion`,docs:{description:`检测缺少 @Injectable() 装饰器的 provider 类`}},createOnce(e){return{before(){let t=e.filename;if(C(t)||w(t)||T(t)||ye(t))return!1},ClassDeclaration(t){let n=f(t);n&&m(n)&&(Ve(t)||Ue(t)||We(t)&&(n.endsWith(`Factory`)&&Ge(t)||ze.some(e=>N(t,e))||e.report({node:t,message:`类 '${n}' 看起来像 provider 但缺少 @Injectable()`})))}}}}),qe={onModuleInit:`OnModuleInit`,onModuleDestroy:`OnModuleDestroy`,onApplicationBootstrap:`OnApplicationBootstrap`,onApplicationShutdown:`OnApplicationShutdown`,beforeApplicationShutdown:`BeforeApplicationShutdown`},Je=e({meta:{type:`suggestion`,docs:{description:`检测缺少生命周期钩子接口声明的类`}},createOnce(e){return{ClassDeclaration(t){let n=f(t)??`<anonymous>`,r=ee(t),i=h(t)?.body??[];for(let t of i){if(t.type!==`MethodDefinition`)continue;let i=p(t);if(!i)continue;let a=qe[i];a&&(r.includes(a)||e.report({node:t,message:`'${n}.${i}()' 缺少 'implements ${a}'`}))}}}}});function Ye(e){return typeof e!=`object`||!e?!1:`value`in e}function Xe(e,t,n,r){let i=h(e)?.body??[];for(let e of i){if(e.type!==`MethodDefinition`)continue;let i=M(e);for(let a of i){if(!O.has(a.name))continue;let i=``;if(a.arguments.length>0&&a.arguments[0]){let e=a.arguments[0];if(e.type===`Literal`&&Ye(e)){let t=e.value;typeof t==`string`&&(i=t)}}let o=[n,i].filter(Boolean).join(`/`).replace(/\/+/g,`/`)||`/`,s=`${a.name}:${o}`,c={method:a.name,path:o,controllerName:t,handlerName:p(e)??`<computed>`,node:e},l=r.get(s)??[];l.push(c),r.set(s,l)}}}function Ze(e,t){for(let[,n]of e)if(n.length>1)for(let e of n)t.report({node:e.node,message:`重复路由: ${e.method} ${e.path} 在 ${e.controllerName}.${e.handlerName}()`})}const Qe=e({meta:{type:`suggestion`,docs:{description:`检测单文件内的重复路由`}},createOnce(e){return{before(){let t=e.filename;if(C(t)||w(t)||T(t)||E(t))return!1},ClassDeclaration(t){let n=P(t,`Controller`);if(!n)return;let r=f(t)??`<anonymous>`,i=Re(n)??``,a=new Map;Xe(t,r,i,a),Ze(a,e)}}}}),$e=e({meta:{type:`suggestion`,docs:{description:`检测未配置全局异常过滤器的 NestJS 应用`}},createOnce(e){let t=e.sourceCode.text;return!t.includes(`NestFactory`)||t.includes(`useGlobalFilters`)||t.includes(`APP_FILTER`)||t.includes(`AllExceptionsFilter`)||t.includes(`HttpExceptionFilter`)||t.includes(`GlobalExceptionFilter`)?{}:{Program(t){e.report({node:t,message:`未配置全局异常过滤器 — 请使用 useGlobalFilters() 或 APP_FILTER provider`})}}}}),F=/(?:^|[._])(?:password|passwd|secret|api[_-]?key|auth[_-]?token|access[_-]?token|refresh[_-]?token|private[_-]?key|client[_-]?secret|db[_-]?pass(?:word)?|jwt[_-]?secret|encryption[_-]?key|signing[_-]?key|secret[_-]?key|connection[_-]?string)$/i,et=/^(true|false|null|undefined|none|default|test|example|placeholder|changeme|todo|fixme|xxx|mock|fake|dummy|sample|dev|local|development|staging|sandbox|demo|your[_-]?secret|replace[_-]?me|insert[_-]?here|<[^>]+>|\$\{[^}]+\}|%[^%]+%)$/i;function I(e){return!e||typeof e!=`object`?!1:`type`in e&&e.type===`Identifier`}function tt(e){return!e||typeof e!=`object`?!1:`type`in e&&e.type===`Literal`}function nt(e){return!e||typeof e!=`object`?!1:`type`in e&&e.type===`TemplateLiteral`}function rt(e){return`value`in e&&typeof e.value==`string`?e.value:null}function it(e){if(e.expressions.length===0&&e.quasis.length===1){let t=e.quasis[0];if(t&&`value`in t&&`cooked`in t.value)return t.value.cooked}return null}function L(e){return tt(e)?rt(e):nt(e)?it(e):null}function at(e){return`id`in e}function ot(e){return`init`in e}function R(e){return`key`in e}function z(e){return`value`in e}function B(e){return e.length===0||e.length<=3?!1:!et.test(e)}function st(e,t){if(!at(e))return;let n=e.id;if(!I(n))return;let r=n.name;if(!F.test(r)||!ot(e))return;let i=e.init;if(!i||typeof i!=`object`)return;let a=L(i);!a||!B(a)||t.report({node:e,message:`在 '${r}' 中发现硬编码密钥 — 请移至环境变量`})}function ct(e,t){if(!R(e))return;let n=e.key;if(!I(n))return;let r=n.name;if(!F.test(r)||!z(e))return;let i=e.value;if(!i||typeof i!=`object`)return;let a=L(i);!a||!B(a)||t.report({node:e,message:`在 '${r}' 中发现硬编码密钥 — 请移至环境变量`})}function lt(e,t){if(!R(e))return;let n=e.key;if(!I(n))return;let r=n.name;if(!F.test(r)||!z(e))return;let i=e.value;if(!i||typeof i!=`object`)return;let a=L(i);!a||!B(a)||t.report({node:e,message:`在 '${r}' 中发现硬编码密钥 — 请移至环境变量`})}const ut=e({meta:{type:`problem`,docs:{description:`检测硬编码的密钥,应移至环境变量`}},createOnce(e){return C(e.filename)?{}:{VariableDeclarator(t){st(t,e)},ObjectProperty(t){ct(t,e)},PropertyDefinition(t){lt(t,e)}}}}),V=/\b(SELECT\s+\*|SELECT\s+\w+\b|SELECT\s+DISTINCT\b|INSERT\s+INTO\b|UPDATE\s+\w+\s+SET\b|DELETE\s+FROM\b|DROP\s+(TABLE|INDEX|DATABASE)\b|ALTER\s+TABLE\b|TRUNCATE(\s+TABLE)?\b|CREATE\s+(TABLE|INDEX|DATABASE|POLICY)\b)/i,H=new Set([`sql`,`sql`,`SQL`,`raw`,`knex`,`knex`,`sequelize`,`sequelize`,`literal`,`typeorm`,`TypeORM`,`drizzle`,`drizzle`,`kysely`,`kysely`,`slonik`,`slonik`,`mikroorm`,`MikroORM`]),dt=new Set([`Prisma`,`prisma`,`knex`,`knex`,`sequelize`,`sequelize`,`db`,`DB`,`database`,`connection`,`pool`]);function ft(e){return e.type===`Identifier`?H.has(e.name):!!(e.type===`MemberExpression`&&e.property.type===`Identifier`&&(H.has(e.property.name)||e.object.type===`Identifier`&&dt.has(e.object.name)))}function pt(e,t){if(e.type!==`TaggedTemplateExpression`)return;let n=e;if(ft(n.tag))return;let r=n.quasi;if(r.expressions.length===0)return;let i=r.quasis.map(e=>e.value.raw).join("${...}");V.test(i)&&t.report({node:e,message:`使用模板插值的原始 SQL — 请改用参数化查询`})}function mt(e,t){if(e.type!==`BinaryExpression`)return;let n=e;n.operator===`+`&&(n.left.type!==`Literal`||typeof n.left.value!=`string`||V.test(n.left.value)&&t.report({node:e,message:`使用字符串拼接的原始 SQL — 请改用参数化查询`}))}const ht=e({meta:{type:`problem`,docs:{description:`检测 SQL 注入风险,应使用参数化查询`}},createOnce(e){return{TaggedTemplateExpression(t){pt(t,e)},BinaryExpression(t){mt(t,e)}}}}),U=/\b(SELECT\s+\*|SELECT\s+\w+\b|SELECT\s+DISTINCT\b|INSERT\s+INTO\b|UPDATE\s+\w+\s+SET\b|DELETE\s+FROM\b|DROP\s+(TABLE|INDEX|DATABASE)\b|ALTER\s+TABLE\b|TRUNCATE(\s+TABLE)?\b|CREATE\s+(TABLE|INDEX|DATABASE|POLICY)\b)/i,W=new Set([`sql`,`sql`,`SQL`,`raw`,`knex`,`knex`,`sequelize`,`sequelize`,`literal`,`typeorm`,`TypeORM`,`drizzle`,`drizzle`,`kysely`,`kysely`,`slonik`,`slonik`,`mikroorm`,`MikroORM`]),gt=new Set([`Prisma`,`prisma`,`knex`,`knex`,`sequelize`,`sequelize`,`db`,`DB`,`database`,`connection`,`pool`]);function _t(e){return e.type===`Identifier`?W.has(e.name):!!(e.type===`MemberExpression`&&e.property.type===`Identifier`&&(W.has(e.property.name)||e.object.type===`Identifier`&&gt.has(e.object.name)))}function vt(e,t){if(e.type!==`TaggedTemplateExpression`)return;let n=e;if(_t(n.tag)||n.quasi.expressions.length===0)return;let r=n.quasi.quasis.map(e=>e.value.raw).join("${...}");U.test(r)&&t.report({node:e,message:`使用模板插值的原始 SQL — 请改用参数化查询`})}function yt(e,t){if(e.type!==`BinaryExpression`)return;let n=e;n.operator===`+`&&(n.left.type!==`Literal`||typeof n.left.value!=`string`||U.test(n.left.value)&&t.report({node:e,message:`使用字符串拼接的原始 SQL — 请改用参数化查询`}))}const bt=e({meta:{type:`problem`,docs:{description:`检测原始 SQL 查询,应使用参数化查询`}},createOnce(e){return C(e.filename)||w(e.filename)?{}:{TaggedTemplateExpression(t){vt(t,e)},BinaryExpression(t){yt(t,e)}}}}),xt=[/run[_-]?migrations?\.ts$/i,/migrations?\.ts$/i,/data[_-]?source\.ts$/i,/seed(er)?\.ts$/i,/cli\.ts$/i,/console\.ts$/i,/worker\.ts$/i,/job\.ts$/i,/cron\.ts$/i,/task\.ts$/i,/command\.ts$/i,/repl\.ts$/i];function St(e){let t=e.replace(/\\/g,`/`);return xt.some(e=>e.test(t))}const Ct=e({meta:{type:`suggestion`,docs:{description:`检测未配置全局 ValidationPipe 的 NestJS 应用`}},createOnce(e){let t=e.sourceCode.text;return!t.includes(`NestFactory`)||St(e.filename)||t.includes(`createApplicationContext`)||t.includes(`ValidationPipe`)||t.includes(`APP_PIPE`)||t.includes(`useGlobalPipes`)||t.includes(`GlobalValidationPipe`)||t.includes(`CustomValidationPipe`)||/validation.*pipe/i.test(t)?{}:{Program(t){e.report({node:t,message:`应用程序引导缺少全局 ValidationPipe`})}}}}),wt=[/Response(Dto|DTO)$/,/Results?(Dto|DTO)$/,/Output(Dto|DTO)$/,/^(Get|List|Find|Fetch|Read)[A-Z].*?(Dto|DTO)$/,/With(Display|View|Full|Extended|Details?)[A-Z].*?(Dto|DTO)$/,/Details(Dto|DTO)$/,/^Base[A-Z].*?(Dto|DTO)$/,/Fields?(Dto|DTO)$/,/Option(s)?(Dto|DTO)$/,/Info(Dto|DTO)$/,/Summary(Dto|DTO)$/,/Status(Dto|DTO)$/,/For[A-Z][a-zA-Z]*(Dto|DTO)$/,/View(Dto|DTO)$/,/Display(Dto|DTO)$/,/Model(Dto|DTO)$/],Tt=[/^Create[A-Z]/,/^Update[A-Z]/,/^Patch[A-Z]/,/^Delete[A-Z]/,/^Add[A-Z]/,/^Remove[A-Z]/,/^Set[A-Z]/,/Request(Dto|DTO)$/,/Input(Dto|DTO)$/,/Params(Dto|DTO)$/,/Query(Dto|DTO)$/,/Body(Dto|DTO)$/];function G(e){return Tt.some(t=>t.test(e))}function K(e){let t=j(e);for(let e of t)if(e.type===`ObjectExpression`){for(let t of e.properties)if(!(t.type!==`Property`||t.key.type!==`Identifier`||t.key.name!==`readOnly`)&&t.value.type===`Literal`&&_(t.value)&&t.value.value===!0)return!0}return!1}function q(e){for(let t of e){let e=A(t);if((e===`ApiProperty`||e===`ApiPropertyOptional`)&&K(t))return!0}return!1}function Et(e){for(let t of e){let e=A(t);if(!e||e===`ApiProperty`||e===`ApiPropertyOptional`||e===`ApiResponseProperty`)continue;if(e.startsWith(`Is`)||e.startsWith(`Has`)||e.startsWith(`Validate`)||e.startsWith(`Check`)||e.startsWith(`Must`))return!0;let n=j(t);for(let e of n)if(e.type===`ObjectExpression`){for(let t of e.properties)if(!(t.type!==`Property`||t.key.type!==`Identifier`)&&(t.key.name===`message`||t.key.name===`groups`||t.key.name===`each`))return!0}}return!1}function Dt(e){if(!e||typeof e!=`object`||!(`typeAnnotation`in e))return!1;let t=e.typeAnnotation;if(!t||typeof t!=`object`||!(`typeAnnotation`in t))return!1;let n=t.typeAnnotation;if(!n||typeof n!=`object`||!(`typeName`in n))return!1;let r=n.typeName;return!!r&&typeof r==`object`&&`name`in r&&typeof r.name==`string`&&r.name.endsWith(`Entity`)}function Ot(e,t){let n=h(e)?.body??[];for(let e of n){if(e.type!==`MethodDefinition`||!re(e)||!e.static)continue;let n=p(e);if(n&&(n.startsWith(`to`)&&(n.endsWith(`Dto`)||n.endsWith(`DTO`))||n.startsWith(`from`)||n===`toDto`||n===`toDTO`||n===`create`&&kt(e)||t&&n.toLowerCase()===`to${t.toLowerCase()}`||n===`of`&&b(e)||n===`map`||n===`transform`||n===`mapFrom`))return!0}return!1}function kt(e){return!_(e)||!b(e.value)||!Array.isArray(e.value.params)||e.value.params.length===0?!1:Dt(e.value.params[0])}function At(e){if(ee(e).includes(`Partial`))return!0;if(!o(e))return!1;let t=e.implements;if(!Array.isArray(t))return!1;for(let e of t){if(!e||typeof e!=`object`)continue;let t=e;if(!v(t)||typeof t.expression!=`object`)continue;let n=t.expression;if(!n||typeof n!=`object`||!(`type`in n))continue;let r=n;if(r.type===`TSInstantiationExpression`&&v(r)){let e=r.expression;if(e&&typeof e==`object`&&`type`in e&&e.type===`Identifier`&&`name`in e&&e.name===`Partial`)return!0}}return!1}function jt(e){let t=h(e)?.body??[],r=[];for(let e of t){if(e.type!==`PropertyDefinition`||!x(e)||!e.key||typeof e.key!=`object`)continue;let t=e.key;if(!(`type`in t)||t.type!==`Identifier`||!(`name`in t))continue;let i=n(t)?t.name:``;if(!i)continue;let a=`decorators`in e&&Array.isArray(e.decorators)?e.decorators:[];r.push({member:e,decorators:a,propName:i})}return r}function Mt(e){return e.length!==0&&e.every(e=>e.decorators.length===0)}function Nt(e){return e.some(e=>e.decorators.some(e=>A(e)===`ApiResponseProperty`))}function Pt(e){return`readonly`in e}function J(e){return Pt(e)&&e.readonly===!0}function Ft(e,t,n){if(t.length===0||G(e))return!1;let r=!1,i=!1,a=!1;for(let e of t){if(e.decorators.length===0){a=!0;continue}let t=J(e.member);for(let o of e.decorators){let e=A(o);if(e){if(n.has(e))return!1;(e===`ApiProperty`||e===`ApiPropertyOptional`||e===`ApiResponseProperty`||e===`ApiHideProperty`)&&(r=!0,K(o)||t?i=!0:e!==`ApiHideProperty`&&e!==`ApiResponseProperty`&&(a=!0))}}}return r&&!(i&&a)}function It(e,t){let n=!1,r=!1,i=!0,a=0,o=!0,s=0,c=!1;for(let l of e)if(l.decorators.length===0&&(c=!0),l.decorators.some(e=>A(e)===`ApiResponseProperty`)&&(n=!0),J(l.member)?a++:i=!1,q(l.decorators)?s++:o=!1,l.propName===`id`){let{hasApi:e,hasVal:n}=Lt(l,t);e&&!n&&(r=!0)}return n||i&&a>=2||r&&!c||o&&s>=2}function Lt(e,t){return{hasApi:e.decorators.some(e=>{let t=A(e);return t===`ApiProperty`||t===`ApiPropertyOptional`}),hasVal:e.decorators.some(e=>{let n=A(e);return n!==null&&t.has(n)})}}function Rt(e,t,n){let r=jt(e);return r.length===0?!1:wt.some(e=>e.test(t))||Ot(e,t)||At(e)?!0:G(t)?!1:Mt(r)||Nt(r)||Ft(t,r,n)?!0:It(r,n)}const Y=new Set(`IsString.IsNotEmpty.IsEmail.IsUrl.IsUUID.IsDateString.IsPhoneNumber.IsAlpha.IsAlphanumeric.IsAscii.IsBase64.IsCreditCard.IsHexColor.IsIP.IsJSON.IsJWT.IsLowercase.IsUppercase.Length.MinLength.MaxLength.Matches.Contains.NotContains.IsNumber.IsInt.IsPositive.IsNegative.Min.Max.IsDivisibleBy.IsBoolean.IsDate.MinDate.MaxDate.IsArray.ArrayMinSize.ArrayMaxSize.ArrayContains.ArrayNotContains.ArrayNotEmpty.ArrayUnique.IsObject.IsNotEmptyObject.ValidateNested.IsEnum.IsOptional.IsDefined.IsIn.IsNotIn.Equals.NotEquals.Type.Transform.Exclude.Expose`.split(`.`));function zt(e){return e.some(e=>{let t=A(e);return t!==null&&Y.has(t)})}function Bt(e,t){let r=f(e);if(!r||!r.endsWith(`Dto`)&&!r.endsWith(`DTO`)||Rt(e,r,Y))return;let i=h(e)?.body??[];for(let e of i){if(e.type!==`PropertyDefinition`||!(`key`in e)||!e.key||typeof e.key!=`object`)continue;let i=e.key;if(!n(i))continue;let a=i.name,o=`decorators`in e&&Vt(e)?e.decorators??[]:[];q(o)||Et(o)||zt(o)||t.report({node:e,message:`DTO '${r}' 中的属性 '${a}' 没有验证装饰器`})}}function Vt(e){return typeof e==`object`&&!!e&&`decorators`in e}const Ht=e({meta:{type:`suggestion`,docs:{description:`检测 DTO 类中缺少验证装饰器的属性`}},createOnce(e){return{before(){let t=e.filename;return!(C(t)||w(t)||T(t))},ClassDeclaration(t){Bt(t,e)}}}}),Ut=new Set([`fs`]),Wt=new Set([`fs`,`node:fs`]),Gt=new Set(`readFileSync.writeFileSync.appendFileSync.copyFileSync.mkdirSync.rmdirSync.readdirSync.statSync.lstatSync.accessSync.chmodSync.chownSync.renameSync.unlinkSync.existsSync.openSync.closeSync.fstatSync.ftruncateSync.futimesSync.linkSync.symlinkSync.readlinkSync.realpathSync.truncateSync.utimesSync.rmSync.cpSync`.split(`.`));function Kt(e,t,n,r,i){let a=e.split(`.`);if(a.length>=2){let e=a[0];return e&&n.has(e)?!0:e?r.has(e):!1}return i.has(t)}function qt(e,t){return e.includes(`.module.`)||t.endsWith(`Module.ts`)||t.endsWith(`Module.js`)}function Jt(e,t){return e.includes(`/cypress/`)||e.startsWith(`cypress/`)||t.includes(`jest`)||t.includes(`vitest`)}function Yt(e){return e.includes(`@Command(`)||e.includes(`@SubCommand(`)}function Xt(e,t,n){let r=e.source.type===`Literal`&&typeof e.source.value==`string`?e.source.value:null;if(!(!r||!Wt.has(r)))for(let r of e.specifiers)r.type===`ImportDefaultSpecifier`||r.type===`ImportNamespaceSpecifier`?t.add(r.local.name):r.type===`ImportSpecifier`&&n.add(r.local.name)}function Zt(e,t,n,r){if(e.type!==`CallExpression`)return;let i=u(e);if(!i)return;let a=i.split(`.`).pop();!a||!Gt.has(a)||Kt(i,a,Ut,n,r)&&t.report({node:e,message:`同步的 '${a}' 会阻塞事件循环 — 请使用异步版本`})}const Qt=e({meta:{type:`suggestion`,docs:{description:`检测同步文件系统操作,应使用异步版本`}},createOnce(e){let t=new Set,n=new Set;return{before(){let t=e.filename,n=t.replace(/\\/g,`/`),r=n.split(`/`).pop()||``;if(qt(t,r)||T(t)||Jt(n,r))return!1;let i=e.sourceCode.text;return!Yt(i)},ImportDeclaration(e){Xt(e,t,n)},CallExpression(r){Zt(r,e,t,n)}}}}),$t=new Set([`crypto`]),en=new Set([`crypto`,`node:crypto`]),tn=new Set([`pbkdf2Sync`,`scryptSync`,`generateKeyPairSync`,`generateKeySync`,`randomFillSync`]);function nn(e,t,n,r,i){let a=e.split(`.`);if(a.length>=2){let e=a[0];return e&&n.has(e)?!0:e?r.has(e):!1}return i.has(t)}function rn(e,t,n){let r=e.source.type===`Literal`&&typeof e.source.value==`string`?e.source.value:null;if(!(!r||!en.has(r)))for(let r of e.specifiers)r.type===`ImportDefaultSpecifier`||r.type===`ImportNamespaceSpecifier`?t.add(r.local.name):r.type===`ImportSpecifier`&&n.add(r.local.name)}function an(e,t,n,r){if(e.type!==`CallExpression`)return;let i=u(e);if(!i)return;let a=i.split(`.`).pop();!a||!tn.has(a)||nn(i,a,$t,n,r)&&t.report({node:e,message:`阻塞的 '${a}' 在请求路径中 — 请使用异步版本`})}const on=e({meta:{type:`suggestion`,docs:{description:`检测阻塞加密操作,应使用异步版本`}},createOnce(e){let t=new Set,n=new Set;return{ImportDeclaration(e){rn(e,t,n)},CallExpression(r){an(r,e,t,n)}}}}),sn=e({meta:{type:`suggestion`,docs:{description:`检测未配置缓存策略的 NestJS 应用`}},createOnce(e){let t=e.sourceCode.text;return!t.includes(`NestFactory`)&&!t.includes(`AppModule`)||t.includes(`CacheModule`)||t.includes(`CacheInterceptor`)||t.includes(`@Cacheable`)||t.includes(`cache-manager`)||t.includes(`redis`)||t.includes(`memcached`)||t.includes(`ioredis`)||!t.includes(`NestFactory`)?{}:{Program(t){e.report({node:t,message:`未配置缓存策略 — 对于读取密集型工作负载,建议使用 @nestjs/cache-manager`})}}}});function cn(e){return e.callee.type===`MemberExpression`&&e.callee.property.type===`Identifier`?e.callee.property.name:e.callee.type===`Identifier`?e.callee.name:null}function X(e,t,n){if(e.type===`BlockStatement`&&s(e)){for(let r of e.body)if(r.type===`ExpressionStatement`&&r.expression.type===`CallExpression`){let e=cn(r.expression);e&&t.has(e)&&n.report({node:r.expression,message:`'${e}' 在循环中 — 建议使用 findMany/findByIds 批量处理或预加载`})}}}function Z(e,t,n){s(e)&&X(e.body,t,n)}function ln(e,t,n){if(e.type!==`CallExpression`||e.callee.type!==`MemberExpression`||e.callee.property.type!==`Identifier`||e.callee.property.name!==`map`||!u(e)?.endsWith(`.map`)||e.arguments.length===0)return;let r=e.arguments[0];r&&(r.type!==`ArrowFunctionExpression`&&r.type!==`FunctionExpression`||s(r)&&X(r.body,t,n))}const un=e({meta:{type:`suggestion`,docs:{description:`检测循环中的数据库查询,可能存在 N+1 问题`}},createOnce(e){let t=new Set([`findOne`,`findUnique`,`findFirst`,`findOneBy`]);return{before(){let t=e.filename;return!(C(t)||w(t)||T(t))},ForStatement(n){Z(n,t,e)},ForInStatement(n){Z(n,t,e)},ForOfStatement(n){Z(n,t,e)},CallExpression(n){ln(n,t,e)}}}}),dn=e({meta:{type:`suggestion`,docs:{description:`检测路由数量超过 10 个的过大控制器`}},createOnce(e){return{ClassDeclaration(t){if(!N(t,`Controller`))return;let n=0,r=h(t)?.body??[];for(let e of r)e.type===`MethodDefinition`&&M(e).some(e=>O.has(e.name))&&n++;if(n>10){let r=f(t)??`<anonymous>`;e.report({node:t,message:`控制器 '${r}' 有 ${n} 个路由(阈值:10)`})}}}}});function fn(e,t){if(!N(e,`Injectable`))return;let n=e.loc;if(!n)return;let r=n.start.line,i=n.end.line-r+1;if(i>400){let n=f(e)??`<anonymous>`;t.report({node:e,message:`服务 '${n}' 有 ${i} 行(阈值:400)`})}}const pn=e({meta:{type:`suggestion`,docs:{description:`检测代码行数超过 400 行的过大服务`}},createOnce(e){return{before(){let t=e.filename;if(D(t)||C(t)||w(t)||T(t)||E(t))return!1},ClassDeclaration(t){fn(t,e)}}}});function mn(e){return typeof e!=`object`||!e?!1:`key`in e}function hn(e){return typeof e!=`object`||!e?!1:`value`in e}function gn(e){return!(typeof e!=`object`||!e||!(`elements`in e))}function _n(e){return typeof e!=`object`||!e?!1:`name`in e}const vn=e({meta:{type:`suggestion`,docs:{description:`检测单文件内的模块循环依赖`}},createOnce(e){return{ClassDeclaration(t){if(!N(t,`Module`))return;let n=f(t);if(!n)return;let r=P(t,`Module`);if(!r)return;let i=j(r);if(i.length===0)return;let a=i[0];!a||a.type!==`ObjectExpression`||yn(a,`imports`).includes(n)&&e.report({node:t,message:`检测到自引用循环依赖:${n} → ${n}`})}}}});function yn(e,t){let n=[];for(let r of e.properties){if(r.type!==`Property`||!mn(r)||!r.key||typeof r.key!=`object`)continue;let e=r.key;if(!(`type`in e)||e.type!==`Identifier`||!(`name`in e)||!_n(e)||e.name!==t||!hn(r)||!r.value||typeof r.value!=`object`)continue;let i=r.value;if(!(!(`type`in i)||i.type!==`ArrayExpression`||!gn(i))){for(let e of i.elements)if(e&&typeof e==`object`&&`type`in e){let t=e;t.type===`Identifier`&&`name`in t&&typeof t.name==`string`&&n.push(t.name)}}}return n}const bn=new Set([`global`,`globalThis`,`window`,`process.env`]),xn=new Set([`Injectable`,`Controller`,`Module`]);function Sn(e,t){return e===`process`&&t===`env`||e===`console`}function Cn(e){return e.type===`NewExpression`}function wn(e){return e.type===`MemberExpression`}function Tn(e){for(let t of xn)if(N(e,t))return!0;return!1}function En(e,t){if(!Cn(e))return;let n=d(e);n&&(Fe.has(n)||m(n)&&t.report({node:e,message:`直接实例化 '${n}' 绕过了依赖注入 — 应该通过构造函数注入`}))}function Dn(e,t){if(!wn(e)||e.object.type!==`Identifier`)return;let n=e.object.name;if(!bn.has(n)||e.property.type!==`Identifier`)return;let r=e.property.name;Sn(n,r)||t.report({node:e,message:`访问全局状态 '${n}.${r}' — 应该通过依赖注入传递配置`})}function On(e,t){let n=f(e);n&&m(n)&&(Tn(e)||t.report({node:e,message:`类 '${n}' 匹配提供者命名约定但缺少 @Injectable() 装饰器`}))}const kn=e({meta:{type:`suggestion`,docs:{description:`检测绕过依赖注入的模式`}},createOnce(e){return C(e.filename)?{}:{NewExpression(t){En(t,e)},MemberExpression(t){Dn(t,e)},ClassDeclaration(t){On(t,e)}}}});function Q(e){return!e||typeof e!=`object`||!n(e)?null:e.name}function An(e){let t=e;for(;t&&typeof t==`object`&&g(t);){let e=t.parent;if(!e)break;if(!a(e)){t=e;continue}if((e.type===`Property`||e.type===`ObjectProperty`)&&x(e)&&Q(e.key)===`useFactory`||e.type===`MethodDefinition`&&x(e)&&Q(e.key)===`useFactory`)return!0;t=e}return!1}function jn(e,t){if(!a(e)||e.type!==`FunctionDeclaration`||!i(e))return!1;let r=e.id;return r&&typeof r==`object`&&a(r)&&r.type===`Identifier`&&n(r)?t.functionNames.has(r.name):!1}function Mn(e,t){if(!a(e)||e.type!==`VariableDeclarator`||!i(e))return!1;let r=e.id;if(!r||typeof r!=`object`||!a(r)||r.type!==`Identifier`||!n(r))return!1;let o=r.name;if(!t.functionNames.has(o)||!ie(e))return!1;let s=e.init;return!s||typeof s!=`object`||!a(s)?!1:s.type===`ArrowFunctionExpression`||s.type===`FunctionExpression`}function Nn(e,t){if(!a(e)||e.type!==`MethodDefinition`||!x(e))return!1;let r=Q(e.key);if(!r||t.classMethodNames.size===0)return!1;let o=S(e);if(!o||!a(o)||o.type!==`ClassBody`)return!1;let s=S(o);if(!s||!a(s)||s.type!==`ClassDeclaration`||!i(s))return!1;let c=s.id;if(!c||typeof c!=`object`||!a(c)||c.type!==`Identifier`||!n(c))return!1;let l=`${c.name}.${r}`;return t.classMethodNames.has(l)}function Pn(e,t){let n=e;for(;n&&typeof n==`object`&&g(n);){let e=n.parent;if(!e)break;if(jn(e,t)||Mn(e,t)||Nn(e,t))return!0;n=e}return!1}function Fn(e){let t=new Set,n=new Set,r=/useFactory\s*:\s*([A-Za-z_$][\w$]*)/g,i;for(;(i=r.exec(e))!==null;){let e=i[1];e&&t.add(e)}let a=/useFactory\s*:\s*([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)/g;for(;(i=a.exec(e))!==null;){let e=i[1],t=i[2];e&&t&&n.add(`${e}.${t}`)}return{functionNames:t,classMethodNames:n}}function In(e){return!(C(e)||D(e)||E(e)||he(e)||ge(e)||_e(e)||be(e))}const Ln=e({meta:{type:`suggestion`,docs:{description:`检测直接实例化服务类,应使用依赖注入`}},createOnce(e){let t={functionNames:new Set,classMethodNames:new Set};return{before(){let n=e.filename;return t={functionNames:new Set,classMethodNames:new Set},In(n)},Program(n){let r=e.sourceCode.text;t=Fn(r)},NewExpression(n){if(n.type!==`NewExpression`)return;let r=d(n);r&&(Fe.has(r)||An(n)||Pn(n,t)||m(r)&&e.report({node:n,message:`直接实例化 '${r}' — 应该使用依赖注入`}))}}}}),Rn=new Set([`ApiResponse`,`ApiOkResponse`,`ApiCreatedResponse`,`ApiAcceptedResponse`,`ApiNoContentResponse`,`ApiBadRequestResponse`,`ApiUnauthorizedResponse`,`ApiForbiddenResponse`,`ApiNotFoundResponse`,`ApiInternalServerErrorResponse`,`ApiDefaultResponse`]),zn=e({meta:{type:`suggestion`,docs:{description:`检测缺少 Swagger 文档装饰器的路由处理器`}},createOnce(e){return C(e.filename)?{}:{ClassDeclaration(t){if(!N(t,`Controller`))return;let n=h(t)?.body??[];for(let t of n){if(t.type!==`MethodDefinition`)continue;let n=M(t);if(!n.some(e=>O.has(e.name))||N(t,`AllowUnauthenticated`)||N(t,`ApiExcludeEndpoint`))continue;let r=N(t,`ApiOperation`),i=n.some(e=>Rn.has(e.name));if(!r||!i){let n=p(t)??`<computed>`,a=[!r&&`@ApiOperation`,!i&&`@ApiResponse`].filter(Boolean).join(` and `);e.report({node:t,message:`Handler '${n}' is missing ${a}`})}}}}}});function Bn(e){return typeof e!=`object`||!e?!1:`typeName`in e}function Vn(e){return!(typeof e!=`object`||!e||!(`type`in e)||!(`name`in e))}function Hn(e){return!(typeof e!=`object`||!e||!(`typeAnnotation`in e))}function Un(e){return!(!(`returnType`in e)||!e.returnType)}function Wn(e){if(e.type!==`TSTypeReference`||!Bn(e))return null;let t=e.typeName;return typeof t!=`object`||!t||!(`type`in t)||!Vn(t)||t.type!==`Identifier`||typeof t.name!=`string`?null:t.name.endsWith(`Entity`)?t.name:null}function Gn(e){return Wn(e)||(Hn(e)?Gn(e.typeAnnotation):null)}function Kn(e){return M(e).some(e=>O.has(e.name))}function qn(e,t){if(!Un(e))return;let n=Gn(e.returnType.typeAnnotation);if(n){let r=p(e)??`<computed>`;t.report({node:e,message:`Handler '${r}' returns ORM entity '${n}' directly — map to a DTO`})}}const Jn=e({meta:{type:`suggestion`,docs:{description:`检测路由处理器直接返回 ORM 实体类型`}},createOnce(e){return{ClassDeclaration(t){if(!N(t,`Controller`))return;let n=h(t)?.body??[];for(let t of n)t.type===`MethodDefinition`&&Kn(t)&&qn(t,e)}}}}),Yn={Post:{bad:new Set([`200`]),expected:`201 Created`},Delete:{bad:new Set([`200`,`201`]),expected:`204 No Content`}},Xn={OK:`200`,CREATED:`201`,ACCEPTED:`202`,NO_CONTENT:`204`};function Zn(e){return e.type===`Literal`&&`value`in e&&typeof e.value==`number`?{statusCode:String(e.value),statusDisplay:String(e.value)}:{statusCode:void 0,statusDisplay:void 0}}function Qn(e){if(e.type!==`MemberExpression`)return{statusCode:void 0,statusDisplay:void 0};let t=e.object,n=e.property;if(t.type===`Identifier`&&t.name===`HttpStatus`&&n.type===`Identifier`){let e=n.name;return{statusCode:Xn[e],statusDisplay:`HttpStatus.${e}`}}return{statusCode:void 0,statusDisplay:void 0}}function $n(e){let t=Zn(e);return t.statusCode?t:Qn(e)}function er(e,t){for(let[n,{bad:r,expected:i}]of Object.entries(Yn)){if(!N(e,n))continue;let a=P(e,`HttpCode`);if(!a)continue;let o=j(a);if(o.length===0)continue;let s=o[0];if(!s)continue;let{statusCode:c,statusDisplay:l}=$n(s);if(c&&r.has(c)){let r=p(e)??`<computed>`;t.report({node:a,message:`@${n} handler '${r}' uses @HttpCode(${l}) — should return ${i}`})}}}const tr=e({meta:{type:`suggestion`,docs:{description:`检测 @HttpCode() 值与 HTTP 方法语义冲突的处理器`}},createOnce(e){return{ClassDeclaration(t){if(!N(t,`Controller`))return;let n=h(t)?.body??[];for(let t of n)t.type===`MethodDefinition`&&er(t,e)}}}}),nr=new Set([`Error`,`TypeError`,`RangeError`]);function rr(e){return`argument`in e&&e.argument!==null&&e.argument!==void 0}function ir(e){return!a(e)||e.type!==`NewExpression`||!v(e)||typeof e.callee!=`object`||!a(e.callee)||e.callee.type!==`Identifier`?null:e.callee.name}const ar=e({meta:{type:`suggestion`,docs:{description:`检测使用通用异常的控制器,应使用 NestJS HTTP 异常`}},createOnce(e){let t=e.filename;return C(t)||D(t)?{}:{ThrowStatement(t){if(!rr(t))return;let n=t.argument;if(!n)return;let r=ir(n);r&&nr.has(r)&&e.report({node:t,message:`Throwing generic '${r}' — use NestJS HttpException or specific exceptions like BadRequestException`})}}}}),or=/^(find|get|list|fetch|load|search|query)(All|Many|List)?$/i,$=new Set([`limit`,`offset`,`page`,`cursor`,`skip`,`take`,`pagesize`]),sr=e({meta:{type:`suggestion`,docs:{description:`检测返回集合但没有分页参数的 GET 处理器`}},createOnce(e){return{ClassDeclaration(t){if(!N(t,`Controller`))return;let n=h(t)?.body??[];for(let t of n){if(t.type!==`MethodDefinition`||!N(t,`Get`))continue;let n=p(t);n&&or.test(n)&&(fr(t)||e.report({node:t,message:`Handler '${n}' returns a collection without pagination — consider adding limit/offset or cursor parameters`}))}}}}});function cr(e){return!(`name`in e)||typeof e.name!=`string`?!1:$.has(e.name.toLowerCase())}function lr(e){if(!(`parameter`in e))return!1;let t=e.parameter;return!t||typeof t!=`object`||!(`type`in t)||!(`name`in t)||typeof t.name!=`string`?!1:$.has(t.name.toLowerCase())}function ur(e){if(!(`decorators`in e))return!1;let t=e;if(!Array.isArray(t.decorators))return!1;for(let e of t.decorators)if(A(e)===`Query`&&dr(e))return!0;return!1}function dr(e){let t=j(e);if(t.length===0||!t[0])return!1;let n=t[0];return n.type!==`Literal`||!(`value`in n)?!1:typeof n.value==`string`&&$.has(n.value.toLowerCase())}function fr(e){for(let t of e.value.params)if(t.type===`Identifier`&&cr(t)||t.type===`TSParameterProperty`&&lr(t)||ur(t))return!0;return!1}const pr={"no-console-log":Se,"no-process-env-direct":Pe,"missing-injectable":Ke,"lifecycle-hook-interface":Je,"no-duplicate-route":Qe,"missing-exception-filter":$e,"no-hardcoded-secrets":ut,"no-sql-injection":ht,"no-raw-sql":bt,"require-validation-pipe":Ct,"missing-class-validator":Ht,"no-sync-fs":Qt,"no-blocking-crypto":on,"missing-caching":sn,"no-n-plus-one":un,"no-god-controller":dn,"no-god-service":pn,"no-circular-dependency":vn,"no-di-bypass":kn,"no-hardcoded-dependency":Ln,"missing-swagger-decorators":zn,"no-entity-as-response":Jn,"no-inconsistent-http-status":tr,"no-generic-exception":ar,"prefer-pagination":sr},mr=t({meta:{name:`@longzai-intelligence-oxlint/nestjs-analyzer-plugin`},rules:pr});export{mr as nestjsAnalyzerPlugin,pr as nestjsAnalyzerRules};
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@longzai-intelligence-oxlint/nestjs-analyzer-plugin",
3
+ "version": "0.1.0",
4
+ "description": "Oxlint NestJS 分析插件,提供 NestJS 应用深度分析规则",
5
+ "license": "UNLICENSED",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "type": "module",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "lzi-builder",
20
+ "build:prod": "NODE_ENV=production lzi-builder",
21
+ "prepublishOnly": "bun run build:prod",
22
+ "dev": "lzi-builder --watch",
23
+ "clean": "rimraf dist",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "test:coverage": "vitest run --coverage",
27
+ "lint": "oxlint && oxfmt --check",
28
+ "lint:fix": "oxlint --fix && oxfmt",
29
+ "typecheck": "bun run typecheck:app && bun run typecheck:node && bun run typecheck:test",
30
+ "typecheck:app": "tsgo --noEmit -p tsconfig/app.json",
31
+ "typecheck:node": "tsgo --noEmit -p tsconfig/node.json",
32
+ "typecheck:test": "tsgo --noEmit -p tsconfig/test.json"
33
+ },
34
+ "dependencies": {
35
+ "@longzai-intelligence-logger/node": "^0.1.1",
36
+ "@oxlint/plugins": "^1.74.0"
37
+ },
38
+ "devDependencies": {
39
+ "@vitest/coverage-v8": "^4.1.10",
40
+ "typescript": "^6.0.3"
41
+ },
42
+ "peerDependencies": {
43
+ "oxlint": ">=1.0.0"
44
+ }
45
+ }