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 { 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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,3 +119,4 @@ export const VIOLATION_SEVERITY_MAP = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export * from "./rules"
|
export * from "./rules"
|
||||||
|
export * from "./ast-node-types"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user