refactor: update AST strategies to use centralized node type constants

This commit is contained in:
imfozilbek
2025-11-27 19:27:30 +05:00
parent 07e6535633
commit 6b35679f09
10 changed files with 96 additions and 34 deletions

View File

@@ -1,6 +1,10 @@
import { ValueObject } from "./ValueObject"
import { REPOSITORY_VIOLATION_TYPES } from "../../shared/constants/rules"
import { REPOSITORY_FALLBACK_SUGGESTIONS, REPOSITORY_PATTERN_MESSAGES } from "../constants/Messages"
import {
REPOSITORY_FALLBACK_SUGGESTIONS,
REPOSITORY_PATTERN_MESSAGES,
VIOLATION_EXAMPLE_VALUES,
} from "../constants/Messages"
interface RepositoryViolationProps {
readonly violationType:
@@ -105,16 +109,16 @@ export class RepositoryViolation extends ValueObject<RepositoryViolationProps> {
public getMessage(): string {
switch (this.props.violationType) {
case REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE:
return `Repository interface uses ORM-specific type '${this.props.ormType || "unknown"}'. Domain should not depend on infrastructure concerns.`
return `Repository interface uses ORM-specific type '${this.props.ormType || VIOLATION_EXAMPLE_VALUES.UNKNOWN}'. Domain should not depend on infrastructure concerns.`
case REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE:
return `Use case depends on concrete repository '${this.props.repositoryName || "unknown"}' instead of interface. Use dependency inversion.`
return `Use case depends on concrete repository '${this.props.repositoryName || VIOLATION_EXAMPLE_VALUES.UNKNOWN}' instead of interface. Use dependency inversion.`
case REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE:
return `Use case creates repository with 'new ${this.props.repositoryName || "Repository"}()'. Use dependency injection instead.`
case REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME:
return `Repository method '${this.props.methodName || "unknown"}' uses technical name. Use domain language instead.`
return `Repository method '${this.props.methodName || VIOLATION_EXAMPLE_VALUES.UNKNOWN}' uses technical name. Use domain language instead.`
default:
return `Repository pattern violation: ${this.props.details}`
@@ -159,8 +163,8 @@ export class RepositoryViolation extends ValueObject<RepositoryViolationProps> {
REPOSITORY_PATTERN_MESSAGES.STEP_USE_DI,
"",
REPOSITORY_PATTERN_MESSAGES.EXAMPLE_PREFIX,
`❌ Bad: constructor(private repo: ${this.props.repositoryName || "UserRepository"})`,
`✅ Good: constructor(private repo: I${this.props.repositoryName?.replace(/^.*?([A-Z]\w+)$/, "$1") || "UserRepository"})`,
`❌ Bad: constructor(private repo: ${this.props.repositoryName || VIOLATION_EXAMPLE_VALUES.USER_REPOSITORY})`,
`✅ Good: constructor(private repo: I${this.props.repositoryName?.replace(/^.*?([A-Z]\w+)$/, "$1") || VIOLATION_EXAMPLE_VALUES.USER_REPOSITORY})`,
].join("\n")
}
@@ -200,7 +204,7 @@ export class RepositoryViolation extends ValueObject<RepositoryViolationProps> {
REPOSITORY_PATTERN_MESSAGES.STEP_AVOID_TECHNICAL,
"",
REPOSITORY_PATTERN_MESSAGES.EXAMPLE_PREFIX,
`❌ Bad: ${this.props.methodName || "findOne"}()`,
`❌ Bad: ${this.props.methodName || VIOLATION_EXAMPLE_VALUES.FIND_ONE}()`,
`✅ Good: ${finalSuggestion}`,
].join("\n")
}

View File

@@ -1,6 +1,7 @@
import Parser from "tree-sitter"
import { IHardcodeDetector } from "../../domain/services/IHardcodeDetector"
import { HardcodedValue } from "../../domain/value-objects/HardcodedValue"
import { FILE_EXTENSIONS } from "../../shared/constants"
import { CodeParser } from "../parsers/CodeParser"
import { AstBooleanAnalyzer } from "../strategies/AstBooleanAnalyzer"
import { AstConfigObjectAnalyzer } from "../strategies/AstConfigObjectAnalyzer"
@@ -112,9 +113,9 @@ export class HardcodeDetector implements IHardcodeDetector {
* Parses code based on file extension
*/
private parseCode(code: string, filePath: string): Parser.Tree {
if (filePath.endsWith(".tsx")) {
if (filePath.endsWith(FILE_EXTENSIONS.TYPESCRIPT_JSX)) {
return this.parser.parseTsx(code)
} else if (filePath.endsWith(".ts")) {
} else if (filePath.endsWith(FILE_EXTENSIONS.TYPESCRIPT)) {
return this.parser.parseTypeScript(code)
}
return this.parser.parseJavaScript(code)

View File

@@ -1,6 +1,6 @@
import Parser from "tree-sitter"
import { HardcodedValue, HardcodeType } from "../../domain/value-objects/HardcodedValue"
import { DETECTION_VALUES } from "../../shared/constants/rules"
import { DETECTION_VALUES, HARDCODE_TYPES } from "../../shared/constants/rules"
import { AstContextChecker } from "./AstContextChecker"
/**
@@ -83,7 +83,7 @@ export class AstBooleanAnalyzer {
return HardcodedValue.create(
value,
"MAGIC_BOOLEAN" as HardcodeType,
HARDCODE_TYPES.MAGIC_BOOLEAN as HardcodeType,
lineNumber,
column,
context,

View File

@@ -1,6 +1,7 @@
import Parser from "tree-sitter"
import { HardcodedValue, HardcodeType } from "../../domain/value-objects/HardcodedValue"
import { HARDCODE_TYPES } from "../../shared/constants/rules"
import { AST_STRING_TYPES } from "../../shared/constants/ast-node-types"
import { ALLOWED_NUMBERS } from "../constants/defaults"
import { AstContextChecker } from "./AstContextChecker"
@@ -71,7 +72,9 @@ export class AstConfigObjectAnalyzer {
}
if (node.type === "string") {
const stringFragment = node.children.find((c) => c.type === "string_fragment")
const stringFragment = node.children.find(
(c) => c.type === AST_STRING_TYPES.STRING_FRAGMENT,
)
return stringFragment !== undefined && stringFragment.text.length > 3
}

View File

@@ -1,4 +1,10 @@
import Parser from "tree-sitter"
import {
AST_FIELD_NAMES,
AST_IDENTIFIER_TYPES,
AST_MODIFIER_TYPES,
AST_VARIABLE_TYPES,
} from "../../shared/constants/ast-node-types"
/**
* AST context checker for analyzing node contexts
@@ -29,22 +35,26 @@ export class AstContextChecker {
* Helper to check if export statement contains "as const"
*/
private checkExportedConstant(exportNode: Parser.SyntaxNode): boolean {
const declaration = exportNode.childForFieldName("declaration")
const declaration = exportNode.childForFieldName(AST_FIELD_NAMES.DECLARATION)
if (!declaration) {
return false
}
const declarator = this.findDescendant(declaration, "variable_declarator")
if (declaration.type !== "lexical_declaration") {
return false
}
const declarator = this.findDescendant(declaration, AST_VARIABLE_TYPES.VARIABLE_DECLARATOR)
if (!declarator) {
return false
}
const value = declarator.childForFieldName("value")
const value = declarator.childForFieldName(AST_FIELD_NAMES.VALUE)
if (value?.type !== "as_expression") {
return false
}
const asType = value.children.find((c) => c.type === "const")
const asType = value.children.find((c) => c.type === AST_MODIFIER_TYPES.CONST)
return asType !== undefined
}
@@ -83,12 +93,17 @@ export class AstContextChecker {
if (current.type === "call_expression") {
const functionNode =
current.childForFieldName("function") ||
current.children.find((c) => c.type === "identifier" || c.type === "import")
current.childForFieldName(AST_FIELD_NAMES.FUNCTION) ||
current.children.find(
(c) =>
c.type === AST_IDENTIFIER_TYPES.IDENTIFIER ||
c.type === AST_IDENTIFIER_TYPES.IMPORT,
)
if (
functionNode &&
(functionNode.text === "import" || functionNode.type === "import")
(functionNode.text === "import" ||
functionNode.type === AST_IDENTIFIER_TYPES.IMPORT)
) {
return true
}
@@ -229,7 +244,13 @@ export class AstContextChecker {
public getNodeContext(node: Parser.SyntaxNode): string {
let current: Parser.SyntaxNode | null = node
while (current && current.type !== "lexical_declaration" && current.type !== "pair") {
while (
current &&
current.type !== "lexical_declaration" &&
current.type !== "pair" &&
current.type !== "call_expression" &&
current.type !== "return_statement"
) {
current = current.parent
}

View File

@@ -1,6 +1,7 @@
import Parser from "tree-sitter"
import { HardcodedValue, HardcodeType } from "../../domain/value-objects/HardcodedValue"
import { HARDCODE_TYPES } from "../../shared/constants/rules"
import { TIMER_FUNCTIONS } from "../../shared/constants/ast-node-types"
import { ALLOWED_NUMBERS, DETECTION_KEYWORDS } from "../constants/defaults"
import { AstContextChecker } from "./AstContextChecker"
@@ -43,7 +44,12 @@ export class AstNumberAnalyzer {
return false
}
if (this.contextChecker.isInCallExpression(parent, ["setTimeout", "setInterval"])) {
if (
this.contextChecker.isInCallExpression(parent, [
TIMER_FUNCTIONS.SET_TIMEOUT,
TIMER_FUNCTIONS.SET_INTERVAL,
])
) {
return true
}

View File

@@ -1,6 +1,7 @@
import Parser from "tree-sitter"
import { HardcodedValue, HardcodeType } from "../../domain/value-objects/HardcodedValue"
import { CONFIG_KEYWORDS, DETECTION_VALUES, HARDCODE_TYPES } from "../../shared/constants/rules"
import { AST_STRING_TYPES } from "../../shared/constants/ast-node-types"
import { AstContextChecker } from "./AstContextChecker"
import { ValuePatternMatcher } from "./ValuePatternMatcher"
@@ -29,7 +30,9 @@ export class AstStringAnalyzer {
* Analyzes a string node and returns a violation if it's a magic string
*/
public analyze(node: Parser.SyntaxNode, lines: string[]): HardcodedValue | null {
const stringFragment = node.children.find((child) => child.type === "string_fragment")
const stringFragment = node.children.find(
(child) => child.type === AST_STRING_TYPES.STRING_FRAGMENT,
)
if (!stringFragment) {
return null
}
@@ -108,6 +111,7 @@ export class AstStringAnalyzer {
"key",
...CONFIG_KEYWORDS.MESSAGES,
"label",
...CONFIG_KEYWORDS.TECHNICAL,
]
return configKeywords.some((keyword) => context.includes(keyword))

View File

@@ -1,3 +1,5 @@
import { VALUE_PATTERN_TYPES } from "../../shared/constants/ast-node-types"
/**
* Pattern matcher for detecting specific value types
*
@@ -131,40 +133,40 @@ export class ValuePatternMatcher {
| "base64"
| null {
if (this.isEmail(value)) {
return "email"
return VALUE_PATTERN_TYPES.EMAIL
}
if (this.isJwt(value)) {
return "api_key"
return VALUE_PATTERN_TYPES.API_KEY
}
if (this.isApiKey(value)) {
return "api_key"
return VALUE_PATTERN_TYPES.API_KEY
}
if (this.isUrl(value)) {
return "url"
return VALUE_PATTERN_TYPES.URL
}
if (this.isIpAddress(value)) {
return "ip_address"
return VALUE_PATTERN_TYPES.IP_ADDRESS
}
if (this.isFilePath(value)) {
return "file_path"
return VALUE_PATTERN_TYPES.FILE_PATH
}
if (this.isDate(value)) {
return "date"
return VALUE_PATTERN_TYPES.DATE
}
if (this.isUuid(value)) {
return "uuid"
return VALUE_PATTERN_TYPES.UUID
}
if (this.isSemver(value)) {
return "version"
return VALUE_PATTERN_TYPES.VERSION
}
if (this.isHexColor(value)) {
return "color"
}
if (this.isMacAddress(value)) {
return "mac_address"
return VALUE_PATTERN_TYPES.MAC_ADDRESS
}
if (this.isBase64(value)) {
return "base64"
return VALUE_PATTERN_TYPES.BASE64
}
return null
}

View File

@@ -119,3 +119,4 @@ export const VIOLATION_SEVERITY_MAP = {
} as const
export * from "./rules"
export * from "./ast-node-types"

View File

@@ -459,7 +459,27 @@ export const CONFIG_KEYWORDS = {
NETWORK: ["endpoint", "host", "domain", "path", "route"],
DATABASE: ["connection", "database"],
SECURITY: ["config", "secret", "token", "password", "credential"],
MESSAGES: ["message", "error", "warning", "text"],
MESSAGES: [
"message",
"error",
"warning",
"text",
"description",
"suggestion",
"violation",
"expected",
"actual",
],
TECHNICAL: [
"type",
"node",
"declaration",
"definition",
"signature",
"pattern",
"suffix",
"prefix",
],
} as const
/**