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 { ValueObject } from "./ValueObject"
import { REPOSITORY_VIOLATION_TYPES } from "../../shared/constants/rules" 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 { interface RepositoryViolationProps {
readonly violationType: readonly violationType:
@@ -105,16 +109,16 @@ export class RepositoryViolation extends ValueObject<RepositoryViolationProps> {
public getMessage(): string { public getMessage(): string {
switch (this.props.violationType) { switch (this.props.violationType) {
case REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE: 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: 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: case REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE:
return `Use case creates repository with 'new ${this.props.repositoryName || "Repository"}()'. Use dependency injection instead.` return `Use case creates repository with 'new ${this.props.repositoryName || "Repository"}()'. Use dependency injection instead.`
case REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME: 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: default:
return `Repository pattern violation: ${this.props.details}` 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.STEP_USE_DI,
"", "",
REPOSITORY_PATTERN_MESSAGES.EXAMPLE_PREFIX, REPOSITORY_PATTERN_MESSAGES.EXAMPLE_PREFIX,
`❌ Bad: constructor(private repo: ${this.props.repositoryName || "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") || "UserRepository"})`, `✅ Good: constructor(private repo: I${this.props.repositoryName?.replace(/^.*?([A-Z]\w+)$/, "$1") || VIOLATION_EXAMPLE_VALUES.USER_REPOSITORY})`,
].join("\n") ].join("\n")
} }
@@ -200,7 +204,7 @@ export class RepositoryViolation extends ValueObject<RepositoryViolationProps> {
REPOSITORY_PATTERN_MESSAGES.STEP_AVOID_TECHNICAL, REPOSITORY_PATTERN_MESSAGES.STEP_AVOID_TECHNICAL,
"", "",
REPOSITORY_PATTERN_MESSAGES.EXAMPLE_PREFIX, REPOSITORY_PATTERN_MESSAGES.EXAMPLE_PREFIX,
`❌ Bad: ${this.props.methodName || "findOne"}()`, `❌ Bad: ${this.props.methodName || VIOLATION_EXAMPLE_VALUES.FIND_ONE}()`,
`✅ Good: ${finalSuggestion}`, `✅ Good: ${finalSuggestion}`,
].join("\n") ].join("\n")
} }

View File

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

View File

@@ -1,6 +1,6 @@
import Parser from "tree-sitter" import Parser from "tree-sitter"
import { HardcodedValue, HardcodeType } from "../../domain/value-objects/HardcodedValue" 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" import { AstContextChecker } from "./AstContextChecker"
/** /**
@@ -83,7 +83,7 @@ export class AstBooleanAnalyzer {
return HardcodedValue.create( return HardcodedValue.create(
value, value,
"MAGIC_BOOLEAN" as HardcodeType, HARDCODE_TYPES.MAGIC_BOOLEAN as HardcodeType,
lineNumber, lineNumber,
column, column,
context, context,

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import Parser from "tree-sitter" import Parser from "tree-sitter"
import { HardcodedValue, HardcodeType } from "../../domain/value-objects/HardcodedValue" import { HardcodedValue, HardcodeType } from "../../domain/value-objects/HardcodedValue"
import { CONFIG_KEYWORDS, DETECTION_VALUES, HARDCODE_TYPES } from "../../shared/constants/rules" 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 { AstContextChecker } from "./AstContextChecker"
import { ValuePatternMatcher } from "./ValuePatternMatcher" 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 * Analyzes a string node and returns a violation if it's a magic string
*/ */
public analyze(node: Parser.SyntaxNode, lines: string[]): HardcodedValue | null { 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) { if (!stringFragment) {
return null return null
} }
@@ -108,6 +111,7 @@ export class AstStringAnalyzer {
"key", "key",
...CONFIG_KEYWORDS.MESSAGES, ...CONFIG_KEYWORDS.MESSAGES,
"label", "label",
...CONFIG_KEYWORDS.TECHNICAL,
] ]
return configKeywords.some((keyword) => context.includes(keyword)) 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 * Pattern matcher for detecting specific value types
* *
@@ -131,40 +133,40 @@ export class ValuePatternMatcher {
| "base64" | "base64"
| null { | null {
if (this.isEmail(value)) { if (this.isEmail(value)) {
return "email" return VALUE_PATTERN_TYPES.EMAIL
} }
if (this.isJwt(value)) { if (this.isJwt(value)) {
return "api_key" return VALUE_PATTERN_TYPES.API_KEY
} }
if (this.isApiKey(value)) { if (this.isApiKey(value)) {
return "api_key" return VALUE_PATTERN_TYPES.API_KEY
} }
if (this.isUrl(value)) { if (this.isUrl(value)) {
return "url" return VALUE_PATTERN_TYPES.URL
} }
if (this.isIpAddress(value)) { if (this.isIpAddress(value)) {
return "ip_address" return VALUE_PATTERN_TYPES.IP_ADDRESS
} }
if (this.isFilePath(value)) { if (this.isFilePath(value)) {
return "file_path" return VALUE_PATTERN_TYPES.FILE_PATH
} }
if (this.isDate(value)) { if (this.isDate(value)) {
return "date" return VALUE_PATTERN_TYPES.DATE
} }
if (this.isUuid(value)) { if (this.isUuid(value)) {
return "uuid" return VALUE_PATTERN_TYPES.UUID
} }
if (this.isSemver(value)) { if (this.isSemver(value)) {
return "version" return VALUE_PATTERN_TYPES.VERSION
} }
if (this.isHexColor(value)) { if (this.isHexColor(value)) {
return "color" return "color"
} }
if (this.isMacAddress(value)) { if (this.isMacAddress(value)) {
return "mac_address" return VALUE_PATTERN_TYPES.MAC_ADDRESS
} }
if (this.isBase64(value)) { if (this.isBase64(value)) {
return "base64" return VALUE_PATTERN_TYPES.BASE64
} }
return null return null
} }

View File

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

View File

@@ -459,7 +459,27 @@ export const CONFIG_KEYWORDS = {
NETWORK: ["endpoint", "host", "domain", "path", "route"], NETWORK: ["endpoint", "host", "domain", "path", "route"],
DATABASE: ["connection", "database"], DATABASE: ["connection", "database"],
SECURITY: ["config", "secret", "token", "password", "credential"], 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 } as const
/** /**