diff --git a/packages/guardian/src/domain/value-objects/RepositoryViolation.ts b/packages/guardian/src/domain/value-objects/RepositoryViolation.ts index 628f80f..22ff3d4 100644 --- a/packages/guardian/src/domain/value-objects/RepositoryViolation.ts +++ b/packages/guardian/src/domain/value-objects/RepositoryViolation.ts @@ -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 { 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 { 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 { 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") } diff --git a/packages/guardian/src/infrastructure/analyzers/HardcodeDetector.ts b/packages/guardian/src/infrastructure/analyzers/HardcodeDetector.ts index c2517c0..e803f1d 100644 --- a/packages/guardian/src/infrastructure/analyzers/HardcodeDetector.ts +++ b/packages/guardian/src/infrastructure/analyzers/HardcodeDetector.ts @@ -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) diff --git a/packages/guardian/src/infrastructure/strategies/AstBooleanAnalyzer.ts b/packages/guardian/src/infrastructure/strategies/AstBooleanAnalyzer.ts index e365539..72cd21e 100644 --- a/packages/guardian/src/infrastructure/strategies/AstBooleanAnalyzer.ts +++ b/packages/guardian/src/infrastructure/strategies/AstBooleanAnalyzer.ts @@ -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, diff --git a/packages/guardian/src/infrastructure/strategies/AstConfigObjectAnalyzer.ts b/packages/guardian/src/infrastructure/strategies/AstConfigObjectAnalyzer.ts index 615f916..2512f28 100644 --- a/packages/guardian/src/infrastructure/strategies/AstConfigObjectAnalyzer.ts +++ b/packages/guardian/src/infrastructure/strategies/AstConfigObjectAnalyzer.ts @@ -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 } diff --git a/packages/guardian/src/infrastructure/strategies/AstContextChecker.ts b/packages/guardian/src/infrastructure/strategies/AstContextChecker.ts index c226731..7fbd61d 100644 --- a/packages/guardian/src/infrastructure/strategies/AstContextChecker.ts +++ b/packages/guardian/src/infrastructure/strategies/AstContextChecker.ts @@ -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 } diff --git a/packages/guardian/src/infrastructure/strategies/AstNumberAnalyzer.ts b/packages/guardian/src/infrastructure/strategies/AstNumberAnalyzer.ts index 8952e4f..d6d851c 100644 --- a/packages/guardian/src/infrastructure/strategies/AstNumberAnalyzer.ts +++ b/packages/guardian/src/infrastructure/strategies/AstNumberAnalyzer.ts @@ -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 } diff --git a/packages/guardian/src/infrastructure/strategies/AstStringAnalyzer.ts b/packages/guardian/src/infrastructure/strategies/AstStringAnalyzer.ts index c444640..532672b 100644 --- a/packages/guardian/src/infrastructure/strategies/AstStringAnalyzer.ts +++ b/packages/guardian/src/infrastructure/strategies/AstStringAnalyzer.ts @@ -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)) diff --git a/packages/guardian/src/infrastructure/strategies/ValuePatternMatcher.ts b/packages/guardian/src/infrastructure/strategies/ValuePatternMatcher.ts index b394c01..7e09d2a 100644 --- a/packages/guardian/src/infrastructure/strategies/ValuePatternMatcher.ts +++ b/packages/guardian/src/infrastructure/strategies/ValuePatternMatcher.ts @@ -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 } diff --git a/packages/guardian/src/shared/constants/index.ts b/packages/guardian/src/shared/constants/index.ts index 3933516..d7f6d3f 100644 --- a/packages/guardian/src/shared/constants/index.ts +++ b/packages/guardian/src/shared/constants/index.ts @@ -119,3 +119,4 @@ export const VIOLATION_SEVERITY_MAP = { } as const export * from "./rules" +export * from "./ast-node-types" diff --git a/packages/guardian/src/shared/constants/rules.ts b/packages/guardian/src/shared/constants/rules.ts index 1aec7d2..7b22f05 100644 --- a/packages/guardian/src/shared/constants/rules.ts +++ b/packages/guardian/src/shared/constants/rules.ts @@ -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 /**