mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
refactor: update AST strategies to use centralized node type constants
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -119,3 +119,4 @@ export const VIOLATION_SEVERITY_MAP = {
|
||||
} as const
|
||||
|
||||
export * from "./rules"
|
||||
export * from "./ast-node-types"
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user