feat: integrate framework leak detection into analysis pipeline

- Add framework leak detector to AnalyzeProject use case
- Export FrameworkLeakDetector in public API
- Add FRAMEWORK_LEAK rule constant
- Include framework leak violations in analysis response
This commit is contained in:
imfozilbek
2025-11-24 12:53:50 +05:00
parent 0e23938e20
commit 19abff30f0
4 changed files with 323 additions and 0 deletions

View File

@@ -7,10 +7,12 @@ import { IFileScanner } from "./domain/services/IFileScanner"
import { ICodeParser } from "./domain/services/ICodeParser" import { ICodeParser } from "./domain/services/ICodeParser"
import { IHardcodeDetector } from "./domain/services/IHardcodeDetector" import { IHardcodeDetector } from "./domain/services/IHardcodeDetector"
import { INamingConventionDetector } from "./domain/services/INamingConventionDetector" import { INamingConventionDetector } from "./domain/services/INamingConventionDetector"
import { IFrameworkLeakDetector } from "./domain/services/IFrameworkLeakDetector"
import { FileScanner } from "./infrastructure/scanners/FileScanner" import { FileScanner } from "./infrastructure/scanners/FileScanner"
import { CodeParser } from "./infrastructure/parsers/CodeParser" import { CodeParser } from "./infrastructure/parsers/CodeParser"
import { HardcodeDetector } from "./infrastructure/analyzers/HardcodeDetector" import { HardcodeDetector } from "./infrastructure/analyzers/HardcodeDetector"
import { NamingConventionDetector } from "./infrastructure/analyzers/NamingConventionDetector" import { NamingConventionDetector } from "./infrastructure/analyzers/NamingConventionDetector"
import { FrameworkLeakDetector } from "./infrastructure/analyzers/FrameworkLeakDetector"
import { ERROR_MESSAGES } from "./shared/constants" import { ERROR_MESSAGES } from "./shared/constants"
/** /**
@@ -63,11 +65,13 @@ export async function analyzeProject(
const codeParser: ICodeParser = new CodeParser() const codeParser: ICodeParser = new CodeParser()
const hardcodeDetector: IHardcodeDetector = new HardcodeDetector() const hardcodeDetector: IHardcodeDetector = new HardcodeDetector()
const namingConventionDetector: INamingConventionDetector = new NamingConventionDetector() const namingConventionDetector: INamingConventionDetector = new NamingConventionDetector()
const frameworkLeakDetector: IFrameworkLeakDetector = new FrameworkLeakDetector()
const useCase = new AnalyzeProject( const useCase = new AnalyzeProject(
fileScanner, fileScanner,
codeParser, codeParser,
hardcodeDetector, hardcodeDetector,
namingConventionDetector, namingConventionDetector,
frameworkLeakDetector,
) )
const result = await useCase.execute(options) const result = await useCase.execute(options)
@@ -86,5 +90,6 @@ export type {
HardcodeViolation, HardcodeViolation,
CircularDependencyViolation, CircularDependencyViolation,
NamingConventionViolation, NamingConventionViolation,
FrameworkLeakViolation,
ProjectMetrics, ProjectMetrics,
} from "./application/use-cases/AnalyzeProject" } from "./application/use-cases/AnalyzeProject"

View File

@@ -4,6 +4,7 @@ import { IFileScanner } from "../../domain/services/IFileScanner"
import { ICodeParser } from "../../domain/services/ICodeParser" import { ICodeParser } from "../../domain/services/ICodeParser"
import { IHardcodeDetector } from "../../domain/services/IHardcodeDetector" import { IHardcodeDetector } from "../../domain/services/IHardcodeDetector"
import { INamingConventionDetector } from "../../domain/services/INamingConventionDetector" import { INamingConventionDetector } from "../../domain/services/INamingConventionDetector"
import { IFrameworkLeakDetector } from "../../domain/services/IFrameworkLeakDetector"
import { SourceFile } from "../../domain/entities/SourceFile" import { SourceFile } from "../../domain/entities/SourceFile"
import { DependencyGraph } from "../../domain/entities/DependencyGraph" import { DependencyGraph } from "../../domain/entities/DependencyGraph"
import { ProjectPath } from "../../domain/value-objects/ProjectPath" import { ProjectPath } from "../../domain/value-objects/ProjectPath"
@@ -30,6 +31,7 @@ export interface AnalyzeProjectResponse {
hardcodeViolations: HardcodeViolation[] hardcodeViolations: HardcodeViolation[]
circularDependencyViolations: CircularDependencyViolation[] circularDependencyViolations: CircularDependencyViolation[]
namingViolations: NamingConventionViolation[] namingViolations: NamingConventionViolation[]
frameworkLeakViolations: FrameworkLeakViolation[]
metrics: ProjectMetrics metrics: ProjectMetrics
} }
@@ -81,6 +83,18 @@ export interface NamingConventionViolation {
suggestion?: string suggestion?: string
} }
export interface FrameworkLeakViolation {
rule: typeof RULES.FRAMEWORK_LEAK
packageName: string
category: string
categoryDescription: string
file: string
layer: string
line?: number
message: string
suggestion: string
}
export interface ProjectMetrics { export interface ProjectMetrics {
totalFiles: number totalFiles: number
totalFunctions: number totalFunctions: number
@@ -100,6 +114,7 @@ export class AnalyzeProject extends UseCase<
private readonly codeParser: ICodeParser, private readonly codeParser: ICodeParser,
private readonly hardcodeDetector: IHardcodeDetector, private readonly hardcodeDetector: IHardcodeDetector,
private readonly namingConventionDetector: INamingConventionDetector, private readonly namingConventionDetector: INamingConventionDetector,
private readonly frameworkLeakDetector: IFrameworkLeakDetector,
) { ) {
super() super()
} }
@@ -148,6 +163,7 @@ export class AnalyzeProject extends UseCase<
const hardcodeViolations = this.detectHardcode(sourceFiles) const hardcodeViolations = this.detectHardcode(sourceFiles)
const circularDependencyViolations = this.detectCircularDependencies(dependencyGraph) const circularDependencyViolations = this.detectCircularDependencies(dependencyGraph)
const namingViolations = this.detectNamingConventions(sourceFiles) const namingViolations = this.detectNamingConventions(sourceFiles)
const frameworkLeakViolations = this.detectFrameworkLeaks(sourceFiles)
const metrics = this.calculateMetrics(sourceFiles, totalFunctions, dependencyGraph) const metrics = this.calculateMetrics(sourceFiles, totalFunctions, dependencyGraph)
return ResponseDto.ok({ return ResponseDto.ok({
@@ -157,6 +173,7 @@ export class AnalyzeProject extends UseCase<
hardcodeViolations, hardcodeViolations,
circularDependencyViolations, circularDependencyViolations,
namingViolations, namingViolations,
frameworkLeakViolations,
metrics, metrics,
}) })
} catch (error) { } catch (error) {
@@ -319,6 +336,34 @@ export class AnalyzeProject extends UseCase<
return violations return violations
} }
private detectFrameworkLeaks(sourceFiles: SourceFile[]): FrameworkLeakViolation[] {
const violations: FrameworkLeakViolation[] = []
for (const file of sourceFiles) {
const leaks = this.frameworkLeakDetector.detectLeaks(
file.imports,
file.path.relative,
file.layer,
)
for (const leak of leaks) {
violations.push({
rule: RULES.FRAMEWORK_LEAK,
packageName: leak.packageName,
category: leak.category,
categoryDescription: leak.getCategoryDescription(),
file: file.path.relative,
layer: leak.layer,
line: leak.line,
message: leak.getMessage(),
suggestion: leak.getSuggestion(),
})
}
}
return violations
}
private calculateMetrics( private calculateMetrics(
sourceFiles: SourceFile[], sourceFiles: SourceFile[],
totalFunctions: number, totalFunctions: number,

View File

@@ -10,5 +10,6 @@ export type {
ArchitectureViolation, ArchitectureViolation,
HardcodeViolation, HardcodeViolation,
CircularDependencyViolation, CircularDependencyViolation,
FrameworkLeakViolation,
ProjectMetrics, ProjectMetrics,
} from "./api" } from "./api"

View File

@@ -6,6 +6,7 @@ export const RULES = {
HARDCODED_VALUE: "hardcoded-value", HARDCODED_VALUE: "hardcoded-value",
CIRCULAR_DEPENDENCY: "circular-dependency", CIRCULAR_DEPENDENCY: "circular-dependency",
NAMING_CONVENTION: "naming-convention", NAMING_CONVENTION: "naming-convention",
FRAMEWORK_LEAK: "framework-leak",
} as const } as const
/** /**
@@ -124,3 +125,274 @@ export const USE_CASE_VERBS = [
"Reject", "Reject",
"Confirm", "Confirm",
] as const ] as const
/**
* Framework-specific packages that should not be imported in domain layer
* These frameworks create tight coupling and violate Clean Architecture principles
*/
export const FRAMEWORK_PACKAGES = {
ORM: [
"@prisma/client",
"prisma",
"typeorm",
"mongoose",
"sequelize",
"@mikro-orm/core",
"@mikro-orm/mongodb",
"@mikro-orm/postgresql",
"drizzle-orm",
"knex",
"objection",
"bookshelf",
"waterline",
"massive",
"pg",
"mysql",
"mysql2",
"sqlite3",
"better-sqlite3",
"mongodb",
"monk",
"tingodb",
"nedb",
"levelup",
"cassandra-driver",
"couchbase",
"redis-om",
],
WEB_FRAMEWORK: [
"express",
"fastify",
"koa",
"@koa/router",
"koa-router",
"@nestjs/common",
"@nestjs/core",
"@nestjs/platform-express",
"@nestjs/platform-fastify",
"hapi",
"@hapi/hapi",
"restify",
"polka",
"micro",
"next",
"nuxt",
"sails",
"adonis",
"@adonisjs/core",
"loopback",
"@loopback/core",
"feathers",
"@feathersjs/feathers",
"meteor",
"strapi",
"@strapi/strapi",
"total.js",
"actionhero",
],
HTTP_CLIENT: [
"axios",
"node-fetch",
"got",
"superagent",
"request",
"request-promise",
"request-promise-native",
"needle",
"bent",
"phin",
"ky",
"undici",
"@apollo/client",
"graphql-request",
"urql",
"isomorphic-fetch",
"cross-fetch",
"fetch-retry",
"wretch",
"httpie",
],
VALIDATION: [
"joi",
"yup",
"zod",
"class-validator",
"ajv",
"validator",
"express-validator",
"celebrate",
"superstruct",
"io-ts",
"runtypes",
"valibot",
"fastest-validator",
"validatorjs",
"vine",
"@vinejs/vine",
"vest",
"json-schema",
"jsonschema",
],
DI_CONTAINER: [
"inversify",
"tsyringe",
"awilix",
"typedi",
"bottlejs",
"injection-js",
"vue-di",
"angular",
"@angular/core",
"di-ts",
"power-di",
],
LOGGER: [
"winston",
"pino",
"bunyan",
"log4js",
"morgan",
"signale",
"consola",
"roarr",
"loglevel",
"debug",
"npmlog",
"@nestjs/logger",
"fancy-log",
"tracer",
"electron-log",
"simple-node-logger",
],
CACHE: [
"redis",
"ioredis",
"memcached",
"node-cache",
"cache-manager",
"keyv",
"flat-cache",
"lru-cache",
"node-cache-manager",
"quick-lru",
"mem",
"memoizee",
"micro-memoize",
"async-cache",
"cacache",
],
MESSAGE_QUEUE: [
"amqplib",
"bull",
"bullmq",
"bee-queue",
"kafkajs",
"rabbitmq",
"amqp",
"aws-sdk",
"@aws-sdk/client-sqs",
"@aws-sdk/client-sns",
"@azure/service-bus",
"azure-sb",
"@google-cloud/pubsub",
"rsmq",
"mqtt",
"rhea",
"stompit",
"activemq",
"zeromq",
"nanomsg",
"kue",
"agenda",
"bree",
],
EMAIL: [
"nodemailer",
"sendgrid",
"@sendgrid/mail",
"mailgun-js",
"mailgun.js",
"postmark",
"sparkpost",
"ses",
"@aws-sdk/client-ses",
"emailjs",
"email-templates",
"mjml",
"pug",
"handlebars",
"sendmail",
"smtp-server",
"mailparser",
"imap",
"imap-simple",
],
STORAGE: [
"aws-sdk",
"@aws-sdk/client-s3",
"@aws-sdk/s3-request-presigner",
"multer",
"multer-s3",
"@google-cloud/storage",
"@azure/storage-blob",
"azure-storage",
"minio",
"formidable",
"busboy",
"multiparty",
"express-fileupload",
"gridfs-stream",
"s3-upload-stream",
"knox",
"pkgcloud",
"@supabase/storage-js",
"cloudinary",
],
TESTING: [
"jest",
"@jest/globals",
"mocha",
"chai",
"sinon",
"supertest",
"cypress",
"@cypress/vue",
"playwright",
"@playwright/test",
"vitest",
"@vitest/ui",
"ava",
"tap",
"tape",
"jasmine",
"@testing-library/react",
"@testing-library/vue",
"enzyme",
],
TEMPLATE_ENGINE: [
"ejs",
"pug",
"jade",
"handlebars",
"mustache",
"nunjucks",
"dot",
"underscore",
"lodash.template",
"hogan.js",
"swig",
"twig",
"marko",
"squirrelly",
"eta",
],
} as const
/**
* Error messages for framework leak violations
*/
export const FRAMEWORK_LEAK_MESSAGES = {
DOMAIN_IMPORT:
'Domain layer imports framework-specific package "{package}". Use interfaces and dependency injection instead.',
SUGGESTION: "Create an interface in domain layer and implement it in infrastructure layer.",
} as const