mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
231 lines
7.8 KiB
TypeScript
231 lines
7.8 KiB
TypeScript
import Parser from "tree-sitter"
|
|
import { NamingViolation } from "../../../domain/value-objects/NamingViolation"
|
|
import { AST_CLASS_TYPES, AST_FIELD_NAMES } from "../../../shared/constants"
|
|
import { LAYERS, NAMING_VIOLATION_TYPES, USE_CASE_VERBS } from "../../../shared/constants/rules"
|
|
import {
|
|
FILE_SUFFIXES,
|
|
NAMING_ERROR_MESSAGES,
|
|
PATTERN_WORDS,
|
|
} from "../../constants/detectorPatterns"
|
|
|
|
/**
|
|
* AST-based analyzer for detecting class naming violations
|
|
*
|
|
* Analyzes class declaration nodes to ensure proper naming conventions:
|
|
* - Domain layer: PascalCase entities and services (*Service)
|
|
* - Application layer: PascalCase use cases (Verb+Noun), DTOs (*Dto/*Request/*Response)
|
|
* - Infrastructure layer: PascalCase controllers, repositories, services
|
|
*/
|
|
export class AstClassNameAnalyzer {
|
|
/**
|
|
* Analyzes a class declaration node
|
|
*/
|
|
public analyze(
|
|
node: Parser.SyntaxNode,
|
|
layer: string,
|
|
filePath: string,
|
|
_lines: string[],
|
|
): NamingViolation | null {
|
|
if (node.type !== AST_CLASS_TYPES.CLASS_DECLARATION) {
|
|
return null
|
|
}
|
|
|
|
const nameNode = node.childForFieldName(AST_FIELD_NAMES.NAME)
|
|
if (!nameNode) {
|
|
return null
|
|
}
|
|
|
|
const className = nameNode.text
|
|
const lineNumber = nameNode.startPosition.row + 1
|
|
|
|
switch (layer) {
|
|
case LAYERS.DOMAIN:
|
|
return this.checkDomainClass(className, filePath, lineNumber)
|
|
case LAYERS.APPLICATION:
|
|
return this.checkApplicationClass(className, filePath, lineNumber)
|
|
case LAYERS.INFRASTRUCTURE:
|
|
return this.checkInfrastructureClass(className, filePath, lineNumber)
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks domain layer class naming
|
|
*/
|
|
private checkDomainClass(
|
|
className: string,
|
|
filePath: string,
|
|
lineNumber: number,
|
|
): NamingViolation | null {
|
|
if (className.endsWith(FILE_SUFFIXES.SERVICE.replace(".ts", ""))) {
|
|
if (!/^[A-Z][a-zA-Z0-9]*Service$/.test(className)) {
|
|
return NamingViolation.create(
|
|
className,
|
|
NAMING_VIOLATION_TYPES.WRONG_CASE,
|
|
LAYERS.DOMAIN,
|
|
`${filePath}:${String(lineNumber)}`,
|
|
NAMING_ERROR_MESSAGES.DOMAIN_SERVICE_PASCAL_CASE,
|
|
className,
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
if (!/^[A-Z][a-zA-Z0-9]*$/.test(className)) {
|
|
return NamingViolation.create(
|
|
className,
|
|
NAMING_VIOLATION_TYPES.WRONG_CASE,
|
|
LAYERS.DOMAIN,
|
|
`${filePath}:${String(lineNumber)}`,
|
|
NAMING_ERROR_MESSAGES.DOMAIN_ENTITY_PASCAL_CASE,
|
|
className,
|
|
NAMING_ERROR_MESSAGES.USE_PASCAL_CASE,
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Checks application layer class naming
|
|
*/
|
|
private checkApplicationClass(
|
|
className: string,
|
|
filePath: string,
|
|
lineNumber: number,
|
|
): NamingViolation | null {
|
|
if (
|
|
className.endsWith("Dto") ||
|
|
className.endsWith("Request") ||
|
|
className.endsWith("Response")
|
|
) {
|
|
if (!/^[A-Z][a-zA-Z0-9]*(Dto|Request|Response)$/.test(className)) {
|
|
return NamingViolation.create(
|
|
className,
|
|
NAMING_VIOLATION_TYPES.WRONG_SUFFIX,
|
|
LAYERS.APPLICATION,
|
|
`${filePath}:${String(lineNumber)}`,
|
|
NAMING_ERROR_MESSAGES.DTO_PASCAL_CASE,
|
|
className,
|
|
NAMING_ERROR_MESSAGES.USE_DTO_SUFFIX,
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
if (className.endsWith("Mapper")) {
|
|
if (!/^[A-Z][a-zA-Z0-9]*Mapper$/.test(className)) {
|
|
return NamingViolation.create(
|
|
className,
|
|
NAMING_VIOLATION_TYPES.WRONG_SUFFIX,
|
|
LAYERS.APPLICATION,
|
|
`${filePath}:${String(lineNumber)}`,
|
|
NAMING_ERROR_MESSAGES.MAPPER_PASCAL_CASE,
|
|
className,
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
const startsWithVerb = this.startsWithCommonVerb(className)
|
|
const startsWithLowercaseVerb = this.startsWithLowercaseVerb(className)
|
|
if (startsWithVerb) {
|
|
if (!/^[A-Z][a-z]+[A-Z][a-zA-Z0-9]*$/.test(className)) {
|
|
return NamingViolation.create(
|
|
className,
|
|
NAMING_VIOLATION_TYPES.WRONG_VERB_NOUN,
|
|
LAYERS.APPLICATION,
|
|
`${filePath}:${String(lineNumber)}`,
|
|
NAMING_ERROR_MESSAGES.USE_CASE_VERB_NOUN,
|
|
className,
|
|
NAMING_ERROR_MESSAGES.USE_VERB_NOUN,
|
|
)
|
|
}
|
|
} else if (startsWithLowercaseVerb) {
|
|
return NamingViolation.create(
|
|
className,
|
|
NAMING_VIOLATION_TYPES.WRONG_VERB_NOUN,
|
|
LAYERS.APPLICATION,
|
|
`${filePath}:${String(lineNumber)}`,
|
|
NAMING_ERROR_MESSAGES.USE_CASE_VERB_NOUN,
|
|
className,
|
|
NAMING_ERROR_MESSAGES.USE_VERB_NOUN,
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Checks infrastructure layer class naming
|
|
*/
|
|
private checkInfrastructureClass(
|
|
className: string,
|
|
filePath: string,
|
|
lineNumber: number,
|
|
): NamingViolation | null {
|
|
if (className.endsWith("Controller")) {
|
|
if (!/^[A-Z][a-zA-Z0-9]*Controller$/.test(className)) {
|
|
return NamingViolation.create(
|
|
className,
|
|
NAMING_VIOLATION_TYPES.WRONG_SUFFIX,
|
|
LAYERS.INFRASTRUCTURE,
|
|
`${filePath}:${String(lineNumber)}`,
|
|
NAMING_ERROR_MESSAGES.CONTROLLER_PASCAL_CASE,
|
|
className,
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
if (
|
|
className.endsWith(PATTERN_WORDS.REPOSITORY) &&
|
|
!className.startsWith(PATTERN_WORDS.I_PREFIX)
|
|
) {
|
|
if (!/^[A-Z][a-zA-Z0-9]*Repository$/.test(className)) {
|
|
return NamingViolation.create(
|
|
className,
|
|
NAMING_VIOLATION_TYPES.WRONG_SUFFIX,
|
|
LAYERS.INFRASTRUCTURE,
|
|
`${filePath}:${String(lineNumber)}`,
|
|
NAMING_ERROR_MESSAGES.REPOSITORY_IMPL_PASCAL_CASE,
|
|
className,
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
if (className.endsWith("Service") || className.endsWith("Adapter")) {
|
|
if (!/^[A-Z][a-zA-Z0-9]*(Service|Adapter)$/.test(className)) {
|
|
return NamingViolation.create(
|
|
className,
|
|
NAMING_VIOLATION_TYPES.WRONG_SUFFIX,
|
|
LAYERS.INFRASTRUCTURE,
|
|
`${filePath}:${String(lineNumber)}`,
|
|
NAMING_ERROR_MESSAGES.SERVICE_ADAPTER_PASCAL_CASE,
|
|
className,
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Checks if class name starts with a common use case verb
|
|
*/
|
|
private startsWithCommonVerb(className: string): boolean {
|
|
return USE_CASE_VERBS.some((verb) => className.startsWith(verb))
|
|
}
|
|
|
|
/**
|
|
* Checks if class name starts with a lowercase verb (camelCase use case)
|
|
*/
|
|
private startsWithLowercaseVerb(className: string): boolean {
|
|
const lowercaseVerbs = USE_CASE_VERBS.map((verb) => verb.toLowerCase())
|
|
return lowercaseVerbs.some((verb) => className.startsWith(verb))
|
|
}
|
|
}
|