mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
refactor: create AST-based naming analyzers for enhanced detection
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import Parser from "tree-sitter"
|
||||
import { NamingViolation } from "../../../domain/value-objects/NamingViolation"
|
||||
import { AST_FIELD_NAMES, AST_FUNCTION_TYPES, CLASS_KEYWORDS } from "../../../shared/constants"
|
||||
import { NAMING_VIOLATION_TYPES } from "../../../shared/constants/rules"
|
||||
import { NAMING_ERROR_MESSAGES } from "../../constants/detectorPatterns"
|
||||
|
||||
/**
|
||||
* AST-based analyzer for detecting function and method naming violations
|
||||
*
|
||||
* Analyzes function declaration, method definition, and arrow function nodes
|
||||
* to ensure proper naming conventions:
|
||||
* - Functions and methods should be camelCase
|
||||
* - Private methods with underscore prefix are allowed
|
||||
*/
|
||||
export class AstFunctionNameAnalyzer {
|
||||
/**
|
||||
* Analyzes a function or method declaration node
|
||||
*/
|
||||
public analyze(
|
||||
node: Parser.SyntaxNode,
|
||||
layer: string,
|
||||
filePath: string,
|
||||
_lines: string[],
|
||||
): NamingViolation | null {
|
||||
const functionNodeTypes = [
|
||||
AST_FUNCTION_TYPES.FUNCTION_DECLARATION,
|
||||
AST_FUNCTION_TYPES.METHOD_DEFINITION,
|
||||
AST_FUNCTION_TYPES.FUNCTION_SIGNATURE,
|
||||
] as const
|
||||
|
||||
if (!(functionNodeTypes as readonly string[]).includes(node.type)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nameNode = node.childForFieldName(AST_FIELD_NAMES.NAME)
|
||||
if (!nameNode) {
|
||||
return null
|
||||
}
|
||||
|
||||
const functionName = nameNode.text
|
||||
const lineNumber = nameNode.startPosition.row + 1
|
||||
|
||||
if (functionName.startsWith("_")) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (functionName === CLASS_KEYWORDS.CONSTRUCTOR) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!/^[a-z][a-zA-Z0-9]*$/.test(functionName)) {
|
||||
return NamingViolation.create(
|
||||
functionName,
|
||||
NAMING_VIOLATION_TYPES.WRONG_CASE,
|
||||
layer,
|
||||
`${filePath}:${String(lineNumber)}`,
|
||||
NAMING_ERROR_MESSAGES.FUNCTION_CAMEL_CASE,
|
||||
functionName,
|
||||
NAMING_ERROR_MESSAGES.USE_CAMEL_CASE_FUNCTION,
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
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 } from "../../../shared/constants/rules"
|
||||
import { NAMING_ERROR_MESSAGES, PATTERN_WORDS } from "../../constants/detectorPatterns"
|
||||
|
||||
/**
|
||||
* AST-based analyzer for detecting interface naming violations
|
||||
*
|
||||
* Analyzes interface declaration nodes to ensure proper naming conventions:
|
||||
* - Domain layer: Repository interfaces must start with 'I' (e.g., IUserRepository)
|
||||
* - All layers: Interfaces should be PascalCase
|
||||
*/
|
||||
export class AstInterfaceNameAnalyzer {
|
||||
/**
|
||||
* Analyzes an interface declaration node
|
||||
*/
|
||||
public analyze(
|
||||
node: Parser.SyntaxNode,
|
||||
layer: string,
|
||||
filePath: string,
|
||||
_lines: string[],
|
||||
): NamingViolation | null {
|
||||
if (node.type !== AST_CLASS_TYPES.INTERFACE_DECLARATION) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nameNode = node.childForFieldName(AST_FIELD_NAMES.NAME)
|
||||
if (!nameNode) {
|
||||
return null
|
||||
}
|
||||
|
||||
const interfaceName = nameNode.text
|
||||
const lineNumber = nameNode.startPosition.row + 1
|
||||
|
||||
if (!/^[A-Z][a-zA-Z0-9]*$/.test(interfaceName)) {
|
||||
return NamingViolation.create(
|
||||
interfaceName,
|
||||
NAMING_VIOLATION_TYPES.WRONG_CASE,
|
||||
layer,
|
||||
`${filePath}:${String(lineNumber)}`,
|
||||
NAMING_ERROR_MESSAGES.INTERFACE_PASCAL_CASE,
|
||||
interfaceName,
|
||||
NAMING_ERROR_MESSAGES.USE_PASCAL_CASE_INTERFACE,
|
||||
)
|
||||
}
|
||||
|
||||
if (layer === LAYERS.DOMAIN) {
|
||||
return this.checkDomainInterface(interfaceName, filePath, lineNumber)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks domain layer interface naming
|
||||
*/
|
||||
private checkDomainInterface(
|
||||
interfaceName: string,
|
||||
filePath: string,
|
||||
lineNumber: number,
|
||||
): NamingViolation | null {
|
||||
if (interfaceName.endsWith(PATTERN_WORDS.REPOSITORY)) {
|
||||
if (!interfaceName.startsWith(PATTERN_WORDS.I_PREFIX)) {
|
||||
return NamingViolation.create(
|
||||
interfaceName,
|
||||
NAMING_VIOLATION_TYPES.WRONG_PREFIX,
|
||||
LAYERS.DOMAIN,
|
||||
`${filePath}:${String(lineNumber)}`,
|
||||
NAMING_ERROR_MESSAGES.REPOSITORY_INTERFACE_I_PREFIX,
|
||||
interfaceName,
|
||||
`Rename to I${interfaceName}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!/^I[A-Z][a-zA-Z0-9]*Repository$/.test(interfaceName)) {
|
||||
return NamingViolation.create(
|
||||
interfaceName,
|
||||
NAMING_VIOLATION_TYPES.WRONG_CASE,
|
||||
LAYERS.DOMAIN,
|
||||
`${filePath}:${String(lineNumber)}`,
|
||||
NAMING_ERROR_MESSAGES.REPOSITORY_INTERFACE_PATTERN,
|
||||
interfaceName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import Parser from "tree-sitter"
|
||||
import { NamingViolation } from "../../../domain/value-objects/NamingViolation"
|
||||
import { AST_CLASS_TYPES, AST_FUNCTION_TYPES, AST_VARIABLE_TYPES } from "../../../shared/constants"
|
||||
import { AstClassNameAnalyzer } from "./AstClassNameAnalyzer"
|
||||
import { AstFunctionNameAnalyzer } from "./AstFunctionNameAnalyzer"
|
||||
import { AstInterfaceNameAnalyzer } from "./AstInterfaceNameAnalyzer"
|
||||
import { AstVariableNameAnalyzer } from "./AstVariableNameAnalyzer"
|
||||
|
||||
/**
|
||||
* AST tree traverser for detecting naming convention violations
|
||||
*
|
||||
* Walks through the Abstract Syntax Tree and uses analyzers
|
||||
* to detect naming violations in classes, interfaces, functions, and variables.
|
||||
*/
|
||||
export class AstNamingTraverser {
|
||||
constructor(
|
||||
private readonly classAnalyzer: AstClassNameAnalyzer,
|
||||
private readonly interfaceAnalyzer: AstInterfaceNameAnalyzer,
|
||||
private readonly functionAnalyzer: AstFunctionNameAnalyzer,
|
||||
private readonly variableAnalyzer: AstVariableNameAnalyzer,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Traverses the AST tree and collects naming violations
|
||||
*/
|
||||
public traverse(
|
||||
tree: Parser.Tree,
|
||||
sourceCode: string,
|
||||
layer: string,
|
||||
filePath: string,
|
||||
): NamingViolation[] {
|
||||
const results: NamingViolation[] = []
|
||||
const lines = sourceCode.split("\n")
|
||||
const cursor = tree.walk()
|
||||
|
||||
this.visit(cursor, lines, layer, filePath, results)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively visits AST nodes
|
||||
*/
|
||||
private visit(
|
||||
cursor: Parser.TreeCursor,
|
||||
lines: string[],
|
||||
layer: string,
|
||||
filePath: string,
|
||||
results: NamingViolation[],
|
||||
): void {
|
||||
const node = cursor.currentNode
|
||||
|
||||
if (node.type === AST_CLASS_TYPES.CLASS_DECLARATION) {
|
||||
const violation = this.classAnalyzer.analyze(node, layer, filePath, lines)
|
||||
if (violation) {
|
||||
results.push(violation)
|
||||
}
|
||||
} else if (node.type === AST_CLASS_TYPES.INTERFACE_DECLARATION) {
|
||||
const violation = this.interfaceAnalyzer.analyze(node, layer, filePath, lines)
|
||||
if (violation) {
|
||||
results.push(violation)
|
||||
}
|
||||
} else if (
|
||||
node.type === AST_FUNCTION_TYPES.FUNCTION_DECLARATION ||
|
||||
node.type === AST_FUNCTION_TYPES.METHOD_DEFINITION ||
|
||||
node.type === AST_FUNCTION_TYPES.FUNCTION_SIGNATURE
|
||||
) {
|
||||
const violation = this.functionAnalyzer.analyze(node, layer, filePath, lines)
|
||||
if (violation) {
|
||||
results.push(violation)
|
||||
}
|
||||
} else if (
|
||||
node.type === AST_VARIABLE_TYPES.VARIABLE_DECLARATOR ||
|
||||
node.type === AST_VARIABLE_TYPES.REQUIRED_PARAMETER ||
|
||||
node.type === AST_VARIABLE_TYPES.OPTIONAL_PARAMETER ||
|
||||
node.type === AST_VARIABLE_TYPES.PUBLIC_FIELD_DEFINITION ||
|
||||
node.type === AST_VARIABLE_TYPES.PROPERTY_SIGNATURE
|
||||
) {
|
||||
const violation = this.variableAnalyzer.analyze(node, layer, filePath, lines)
|
||||
if (violation) {
|
||||
results.push(violation)
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor.gotoFirstChild()) {
|
||||
do {
|
||||
this.visit(cursor, lines, layer, filePath, results)
|
||||
} while (cursor.gotoNextSibling())
|
||||
cursor.gotoParent()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import Parser from "tree-sitter"
|
||||
import { NamingViolation } from "../../../domain/value-objects/NamingViolation"
|
||||
import {
|
||||
AST_FIELD_NAMES,
|
||||
AST_FIELD_TYPES,
|
||||
AST_MODIFIER_TYPES,
|
||||
AST_PATTERN_TYPES,
|
||||
AST_STATEMENT_TYPES,
|
||||
AST_VARIABLE_TYPES,
|
||||
} from "../../../shared/constants"
|
||||
import { NAMING_VIOLATION_TYPES } from "../../../shared/constants/rules"
|
||||
import { NAMING_ERROR_MESSAGES } from "../../constants/detectorPatterns"
|
||||
|
||||
/**
|
||||
* AST-based analyzer for detecting variable naming violations
|
||||
*
|
||||
* Analyzes variable declarations to ensure proper naming conventions:
|
||||
* - Regular variables: camelCase
|
||||
* - Constants (exported UPPER_CASE): UPPER_SNAKE_CASE
|
||||
* - Class properties: camelCase
|
||||
* - Private properties with underscore prefix are allowed
|
||||
*/
|
||||
export class AstVariableNameAnalyzer {
|
||||
/**
|
||||
* Analyzes a variable declaration node
|
||||
*/
|
||||
public analyze(
|
||||
node: Parser.SyntaxNode,
|
||||
layer: string,
|
||||
filePath: string,
|
||||
_lines: string[],
|
||||
): NamingViolation | null {
|
||||
const variableNodeTypes = [
|
||||
AST_VARIABLE_TYPES.VARIABLE_DECLARATOR,
|
||||
AST_VARIABLE_TYPES.REQUIRED_PARAMETER,
|
||||
AST_VARIABLE_TYPES.OPTIONAL_PARAMETER,
|
||||
AST_VARIABLE_TYPES.PUBLIC_FIELD_DEFINITION,
|
||||
AST_VARIABLE_TYPES.PROPERTY_SIGNATURE,
|
||||
] as const
|
||||
|
||||
if (!(variableNodeTypes as readonly string[]).includes(node.type)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nameNode = node.childForFieldName(AST_FIELD_NAMES.NAME)
|
||||
if (!nameNode) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.isDestructuringPattern(nameNode)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const variableName = nameNode.text
|
||||
const lineNumber = nameNode.startPosition.row + 1
|
||||
|
||||
if (variableName.startsWith("_")) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isConstant = this.isConstantVariable(node)
|
||||
|
||||
if (isConstant) {
|
||||
if (!/^[A-Z][A-Z0-9_]*$/.test(variableName)) {
|
||||
return NamingViolation.create(
|
||||
variableName,
|
||||
NAMING_VIOLATION_TYPES.WRONG_CASE,
|
||||
layer,
|
||||
`${filePath}:${String(lineNumber)}`,
|
||||
NAMING_ERROR_MESSAGES.CONSTANT_UPPER_SNAKE_CASE,
|
||||
variableName,
|
||||
NAMING_ERROR_MESSAGES.USE_UPPER_SNAKE_CASE_CONSTANT,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (!/^[a-z][a-zA-Z0-9]*$/.test(variableName)) {
|
||||
return NamingViolation.create(
|
||||
variableName,
|
||||
NAMING_VIOLATION_TYPES.WRONG_CASE,
|
||||
layer,
|
||||
`${filePath}:${String(lineNumber)}`,
|
||||
NAMING_ERROR_MESSAGES.VARIABLE_CAMEL_CASE,
|
||||
variableName,
|
||||
NAMING_ERROR_MESSAGES.USE_CAMEL_CASE_VARIABLE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if node is a destructuring pattern (object or array)
|
||||
*/
|
||||
private isDestructuringPattern(node: Parser.SyntaxNode): boolean {
|
||||
return (
|
||||
node.type === AST_PATTERN_TYPES.OBJECT_PATTERN ||
|
||||
node.type === AST_PATTERN_TYPES.ARRAY_PATTERN
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a variable is a constant (exported UPPER_CASE)
|
||||
*/
|
||||
private isConstantVariable(node: Parser.SyntaxNode): boolean {
|
||||
const variableName = node.childForFieldName(AST_FIELD_NAMES.NAME)?.text
|
||||
if (!variableName || !/^[A-Z]/.test(variableName)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
node.type === AST_VARIABLE_TYPES.PUBLIC_FIELD_DEFINITION ||
|
||||
node.type === AST_FIELD_TYPES.FIELD_DEFINITION
|
||||
) {
|
||||
return this.hasConstModifiers(node)
|
||||
}
|
||||
|
||||
let current: Parser.SyntaxNode | null = node.parent
|
||||
|
||||
while (current) {
|
||||
if (current.type === AST_STATEMENT_TYPES.LEXICAL_DECLARATION) {
|
||||
const firstChild = current.child(0)
|
||||
if (firstChild?.type === AST_MODIFIER_TYPES.CONST) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
current.type === AST_VARIABLE_TYPES.PUBLIC_FIELD_DEFINITION ||
|
||||
current.type === AST_FIELD_TYPES.FIELD_DEFINITION
|
||||
) {
|
||||
return this.hasConstModifiers(current)
|
||||
}
|
||||
|
||||
current = current.parent
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if field has readonly or static modifiers (indicating a constant)
|
||||
*/
|
||||
private hasConstModifiers(fieldNode: Parser.SyntaxNode): boolean {
|
||||
for (let i = 0; i < fieldNode.childCount; i++) {
|
||||
const child = fieldNode.child(i)
|
||||
const childText = child?.text
|
||||
if (
|
||||
child?.type === AST_MODIFIER_TYPES.READONLY ||
|
||||
child?.type === AST_MODIFIER_TYPES.STATIC ||
|
||||
childText === AST_MODIFIER_TYPES.READONLY ||
|
||||
childText === AST_MODIFIER_TYPES.STATIC
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user