Files
puaros/packages/guardian/src/infrastructure/strategies/naming/AstVariableNameAnalyzer.ts

160 lines
5.0 KiB
TypeScript

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
}
}