mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
Compare commits
3 Commits
ipuaro-v0.
...
ipuaro-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeaa223436 | ||
|
|
36768c06d1 | ||
|
|
5a22cd5c9b |
@@ -5,6 +5,66 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.26.0] - 2025-12-05 - Rich Initial Context: Decorator Extraction
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Decorator Extraction (0.24.4)**
|
||||||
|
- Functions now show their decorators in initial context
|
||||||
|
- Classes now show their decorators in initial context
|
||||||
|
- Methods show decorators per-method
|
||||||
|
- New format: `@Controller('users') class UserController`
|
||||||
|
- Function format: `@Get(':id') async getUser(id: string): Promise<User>`
|
||||||
|
- Supports NestJS decorators: `@Controller`, `@Get`, `@Post`, `@Injectable`, `@UseGuards`, etc.
|
||||||
|
- Supports Angular decorators: `@Component`, `@Injectable`, `@Input`, `@Output`, etc.
|
||||||
|
|
||||||
|
- **FileAST.ts Enhancements**
|
||||||
|
- `decorators?: string[]` field on `FunctionInfo`
|
||||||
|
- `decorators?: string[]` field on `MethodInfo`
|
||||||
|
- `decorators?: string[]` field on `ClassInfo`
|
||||||
|
|
||||||
|
- **ASTParser.ts Enhancements**
|
||||||
|
- `formatDecorator()` - formats decorator node to string (e.g., `@Get(':id')`)
|
||||||
|
- `extractNodeDecorators()` - extracts decorators that are direct children of a node
|
||||||
|
- `extractDecoratorsFromSiblings()` - extracts decorators before the declaration in export statements
|
||||||
|
- Decorators are extracted for classes, methods, and exported functions
|
||||||
|
|
||||||
|
- **prompts.ts Enhancements**
|
||||||
|
- `formatDecoratorsPrefix()` - formats decorators as a prefix string for display
|
||||||
|
- Used in `formatFunctionSignature()` for function decorators
|
||||||
|
- Used in `formatFileSummary()` for class decorators
|
||||||
|
|
||||||
|
### New Context Format
|
||||||
|
|
||||||
|
```
|
||||||
|
### src/controllers/user.controller.ts
|
||||||
|
- @Controller('users') class UserController extends BaseController
|
||||||
|
- @Get(':id') @Auth() async getUser(id: string): Promise<User>
|
||||||
|
- @Post() @ValidateBody() async createUser(data: UserDTO): Promise<User>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- Total tests: 1754 passed (was 1720, +34 new tests)
|
||||||
|
- 14 new tests for ASTParser decorator extraction
|
||||||
|
- 6 new tests for prompts decorator formatting
|
||||||
|
- +14 other tests from internal improvements
|
||||||
|
- Coverage: 97.49% lines, 91.14% branches, 98.61% functions
|
||||||
|
- 0 ESLint errors, 2 warnings (pre-existing complexity in ASTParser and prompts)
|
||||||
|
- Build successful
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
This completes the v0.24.0 Rich Initial Context milestone:
|
||||||
|
- ✅ 0.24.1 - Function Signatures with Types
|
||||||
|
- ✅ 0.24.2 - Interface/Type Field Definitions
|
||||||
|
- ✅ 0.24.3 - Enum Value Definitions
|
||||||
|
- ✅ 0.24.4 - Decorator Extraction
|
||||||
|
|
||||||
|
Next milestone: v0.25.0 - Graph Metrics in Context
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.25.0] - 2025-12-04 - Rich Initial Context: Interface Fields & Type Definitions
|
## [0.25.0] - 2025-12-04 - Rich Initial Context: Interface Fields & Type Definitions
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1779,10 +1779,10 @@ export interface ScanResult {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Version 0.24.0 - Rich Initial Context 📋
|
## Version 0.24.0 - Rich Initial Context 📋 ✅
|
||||||
|
|
||||||
**Priority:** HIGH
|
**Priority:** HIGH
|
||||||
**Status:** In Progress (2/4 complete)
|
**Status:** Complete (v0.24.0 released)
|
||||||
|
|
||||||
Enhance initial context for LLM: add function signatures, interface field types, and enum values. This allows LLM to answer questions about types and parameters without tool calls.
|
Enhance initial context for LLM: add function signatures, interface field types, and enum values. This allows LLM to answer questions about types and parameters without tool calls.
|
||||||
|
|
||||||
@@ -1836,7 +1836,7 @@ Enhance initial context for LLM: add function signatures, interface field types,
|
|||||||
|
|
||||||
**Why:** LLM knows data structure, won't invent fields.
|
**Why:** LLM knows data structure, won't invent fields.
|
||||||
|
|
||||||
### 0.24.3 - Enum Value Definitions
|
### 0.24.3 - Enum Value Definitions ⭐ ✅
|
||||||
|
|
||||||
**Problem:** LLM only sees `type: Status`
|
**Problem:** LLM only sees `type: Status`
|
||||||
**Solution:** Show values: `Status { Active=1, Inactive=0, Pending=2 }`
|
**Solution:** Show values: `Status { Active=1, Inactive=0, Pending=2 }`
|
||||||
@@ -1852,13 +1852,13 @@ Enhance initial context for LLM: add function signatures, interface field types,
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Changes:**
|
**Changes:**
|
||||||
- [ ] Add `EnumInfo` to FileAST with members and values
|
- [x] Add `EnumInfo` to FileAST with members and values
|
||||||
- [ ] Update `ASTParser.ts` to extract enum members
|
- [x] Update `ASTParser.ts` to extract enum members
|
||||||
- [ ] Update `formatFileSummary()` to output enum values
|
- [x] Update `formatFileSummary()` to output enum values
|
||||||
|
|
||||||
**Why:** LLM knows valid enum values.
|
**Why:** LLM knows valid enum values.
|
||||||
|
|
||||||
### 0.24.4 - Decorator Extraction
|
### 0.24.4 - Decorator Extraction ⭐ ✅
|
||||||
|
|
||||||
**Problem:** LLM doesn't see decorators (important for NestJS, Angular)
|
**Problem:** LLM doesn't see decorators (important for NestJS, Angular)
|
||||||
**Solution:** Show decorators in context
|
**Solution:** Show decorators in context
|
||||||
@@ -1872,16 +1872,15 @@ Enhance initial context for LLM: add function signatures, interface field types,
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Changes:**
|
**Changes:**
|
||||||
- [ ] Add `decorators: string[]` to FunctionInfo and ClassInfo
|
- [x] Add `decorators: string[]` to FunctionInfo, MethodInfo, and ClassInfo
|
||||||
- [ ] Update `ASTParser.ts` to extract decorators
|
- [x] Update `ASTParser.ts` to extract decorators via `extractNodeDecorators()` and `extractDecoratorsFromSiblings()`
|
||||||
- [ ] Update context to display decorators
|
- [x] Update `prompts.ts` to display decorators via `formatDecoratorsPrefix()`
|
||||||
|
|
||||||
**Why:** LLM understands routing, DI, guards in NestJS/Angular.
|
**Why:** LLM understands routing, DI, guards in NestJS/Angular.
|
||||||
|
|
||||||
**Tests:**
|
**Tests:**
|
||||||
- [ ] Unit tests for enhanced ASTParser
|
- [x] Unit tests for ASTParser decorator extraction (14 tests)
|
||||||
- [ ] Unit tests for new context format
|
- [x] Unit tests for prompts decorator formatting (6 tests)
|
||||||
- [ ] Integration tests for full flow
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1999,7 +1998,7 @@ interface FileMeta {
|
|||||||
- [x] 0 ESLint errors ✅
|
- [x] 0 ESLint errors ✅
|
||||||
- [x] Examples working ✅ (v0.18.0)
|
- [x] Examples working ✅ (v0.18.0)
|
||||||
- [x] CHANGELOG.md up to date ✅
|
- [x] CHANGELOG.md up to date ✅
|
||||||
- [ ] Rich initial context (v0.24.0) — function signatures, interface fields, enum values
|
- [x] Rich initial context (v0.24.0) — function signatures, interface fields, enum values, decorators ✅
|
||||||
- [ ] Graph metrics in context (v0.25.0) — dependency graph, circular deps, impact score
|
- [ ] Graph metrics in context (v0.25.0) — dependency graph, circular deps, impact score
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -2077,9 +2076,9 @@ sessions:list # List<session_id>
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated:** 2025-12-04
|
**Last Updated:** 2025-12-05
|
||||||
**Target Version:** 1.0.0
|
**Target Version:** 1.0.0
|
||||||
**Current Version:** 0.25.0
|
**Current Version:** 0.26.0
|
||||||
**Next Milestones:** v0.24.0 (Rich Context - 2/4 complete), v0.25.0 (Graph Metrics)
|
**Next Milestones:** v0.25.0 (Graph Metrics - 0/4 complete)
|
||||||
|
|
||||||
> **Note:** v0.24.0 and v0.25.0 are required for 1.0.0 release. They enable LLM to answer questions about types, signatures, and architecture without tool calls.
|
> **Note:** v0.24.0 complete ✅. v0.25.0 (Graph Metrics) is required for 1.0.0 release. It enables LLM to see architecture without tool calls.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@samiyev/ipuaro",
|
"name": "@samiyev/ipuaro",
|
||||||
"version": "0.25.0",
|
"version": "0.26.0",
|
||||||
"description": "Local AI agent for codebase operations with infinite context feeling",
|
"description": "Local AI agent for codebase operations with infinite context feeling",
|
||||||
"author": "Fozilbek Samiyev <fozilbek.samiyev@gmail.com>",
|
"author": "Fozilbek Samiyev <fozilbek.samiyev@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export interface FunctionInfo {
|
|||||||
isExported: boolean
|
isExported: boolean
|
||||||
/** Return type (if available) */
|
/** Return type (if available) */
|
||||||
returnType?: string
|
returnType?: string
|
||||||
|
/** Decorators applied to the function (e.g., ["@Get(':id')", "@Auth()"]) */
|
||||||
|
decorators?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MethodInfo {
|
export interface MethodInfo {
|
||||||
@@ -69,6 +71,8 @@ export interface MethodInfo {
|
|||||||
visibility: "public" | "private" | "protected"
|
visibility: "public" | "private" | "protected"
|
||||||
/** Whether it's static */
|
/** Whether it's static */
|
||||||
isStatic: boolean
|
isStatic: boolean
|
||||||
|
/** Decorators applied to the method (e.g., ["@Get(':id')", "@UseGuards(AuthGuard)"]) */
|
||||||
|
decorators?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PropertyInfo {
|
export interface PropertyInfo {
|
||||||
@@ -105,6 +109,8 @@ export interface ClassInfo {
|
|||||||
isExported: boolean
|
isExported: boolean
|
||||||
/** Whether class is abstract */
|
/** Whether class is abstract */
|
||||||
isAbstract: boolean
|
isAbstract: boolean
|
||||||
|
/** Decorators applied to the class (e.g., ["@Controller('users')", "@Injectable()"]) */
|
||||||
|
decorators?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InterfaceInfo {
|
export interface InterfaceInfo {
|
||||||
@@ -133,6 +139,28 @@ export interface TypeAliasInfo {
|
|||||||
definition?: string
|
definition?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EnumMemberInfo {
|
||||||
|
/** Member name */
|
||||||
|
name: string
|
||||||
|
/** Member value (string or number, if specified) */
|
||||||
|
value?: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnumInfo {
|
||||||
|
/** Enum name */
|
||||||
|
name: string
|
||||||
|
/** Start line number */
|
||||||
|
lineStart: number
|
||||||
|
/** End line number */
|
||||||
|
lineEnd: number
|
||||||
|
/** Enum members with values */
|
||||||
|
members: EnumMemberInfo[]
|
||||||
|
/** Whether it's exported */
|
||||||
|
isExported: boolean
|
||||||
|
/** Whether it's a const enum */
|
||||||
|
isConst: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface FileAST {
|
export interface FileAST {
|
||||||
/** Import statements */
|
/** Import statements */
|
||||||
imports: ImportInfo[]
|
imports: ImportInfo[]
|
||||||
@@ -146,6 +174,8 @@ export interface FileAST {
|
|||||||
interfaces: InterfaceInfo[]
|
interfaces: InterfaceInfo[]
|
||||||
/** Type alias declarations */
|
/** Type alias declarations */
|
||||||
typeAliases: TypeAliasInfo[]
|
typeAliases: TypeAliasInfo[]
|
||||||
|
/** Enum declarations */
|
||||||
|
enums: EnumInfo[]
|
||||||
/** Whether parsing encountered errors */
|
/** Whether parsing encountered errors */
|
||||||
parseError: boolean
|
parseError: boolean
|
||||||
/** Parse error message if any */
|
/** Parse error message if any */
|
||||||
@@ -160,6 +190,7 @@ export function createEmptyFileAST(): FileAST {
|
|||||||
classes: [],
|
classes: [],
|
||||||
interfaces: [],
|
interfaces: [],
|
||||||
typeAliases: [],
|
typeAliases: [],
|
||||||
|
enums: [],
|
||||||
parseError: false,
|
parseError: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import JSON from "tree-sitter-json"
|
|||||||
import * as yamlParser from "yaml"
|
import * as yamlParser from "yaml"
|
||||||
import {
|
import {
|
||||||
createEmptyFileAST,
|
createEmptyFileAST,
|
||||||
|
type EnumMemberInfo,
|
||||||
type ExportInfo,
|
type ExportInfo,
|
||||||
type FileAST,
|
type FileAST,
|
||||||
type ImportInfo,
|
type ImportInfo,
|
||||||
@@ -192,6 +193,11 @@ export class ASTParser {
|
|||||||
this.extractTypeAlias(node, ast, false)
|
this.extractTypeAlias(node, ast, false)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case NodeType.ENUM_DECLARATION:
|
||||||
|
if (isTypeScript) {
|
||||||
|
this.extractEnum(node, ast, false)
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,13 +264,15 @@ export class ASTParser {
|
|||||||
const declaration = node.childForFieldName(FieldName.DECLARATION)
|
const declaration = node.childForFieldName(FieldName.DECLARATION)
|
||||||
|
|
||||||
if (declaration) {
|
if (declaration) {
|
||||||
|
const decorators = this.extractDecoratorsFromSiblings(declaration)
|
||||||
|
|
||||||
switch (declaration.type) {
|
switch (declaration.type) {
|
||||||
case NodeType.FUNCTION_DECLARATION:
|
case NodeType.FUNCTION_DECLARATION:
|
||||||
this.extractFunction(declaration, ast, true)
|
this.extractFunction(declaration, ast, true, decorators)
|
||||||
this.addExportInfo(ast, declaration, "function", isDefault)
|
this.addExportInfo(ast, declaration, "function", isDefault)
|
||||||
break
|
break
|
||||||
case NodeType.CLASS_DECLARATION:
|
case NodeType.CLASS_DECLARATION:
|
||||||
this.extractClass(declaration, ast, true)
|
this.extractClass(declaration, ast, true, decorators)
|
||||||
this.addExportInfo(ast, declaration, "class", isDefault)
|
this.addExportInfo(ast, declaration, "class", isDefault)
|
||||||
break
|
break
|
||||||
case NodeType.INTERFACE_DECLARATION:
|
case NodeType.INTERFACE_DECLARATION:
|
||||||
@@ -275,6 +283,10 @@ export class ASTParser {
|
|||||||
this.extractTypeAlias(declaration, ast, true)
|
this.extractTypeAlias(declaration, ast, true)
|
||||||
this.addExportInfo(ast, declaration, "type", isDefault)
|
this.addExportInfo(ast, declaration, "type", isDefault)
|
||||||
break
|
break
|
||||||
|
case NodeType.ENUM_DECLARATION:
|
||||||
|
this.extractEnum(declaration, ast, true)
|
||||||
|
this.addExportInfo(ast, declaration, "type", isDefault)
|
||||||
|
break
|
||||||
case NodeType.LEXICAL_DECLARATION:
|
case NodeType.LEXICAL_DECLARATION:
|
||||||
this.extractLexicalDeclaration(declaration, ast, true)
|
this.extractLexicalDeclaration(declaration, ast, true)
|
||||||
break
|
break
|
||||||
@@ -299,7 +311,12 @@ export class ASTParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractFunction(node: SyntaxNode, ast: FileAST, isExported: boolean): void {
|
private extractFunction(
|
||||||
|
node: SyntaxNode,
|
||||||
|
ast: FileAST,
|
||||||
|
isExported: boolean,
|
||||||
|
externalDecorators: string[] = [],
|
||||||
|
): void {
|
||||||
const nameNode = node.childForFieldName(FieldName.NAME)
|
const nameNode = node.childForFieldName(FieldName.NAME)
|
||||||
if (!nameNode) {
|
if (!nameNode) {
|
||||||
return
|
return
|
||||||
@@ -309,6 +326,9 @@ export class ASTParser {
|
|||||||
const isAsync = node.children.some((c) => c.type === NodeType.ASYNC)
|
const isAsync = node.children.some((c) => c.type === NodeType.ASYNC)
|
||||||
const returnTypeNode = node.childForFieldName(FieldName.RETURN_TYPE)
|
const returnTypeNode = node.childForFieldName(FieldName.RETURN_TYPE)
|
||||||
|
|
||||||
|
const nodeDecorators = this.extractNodeDecorators(node)
|
||||||
|
const decorators = [...externalDecorators, ...nodeDecorators]
|
||||||
|
|
||||||
ast.functions.push({
|
ast.functions.push({
|
||||||
name: nameNode.text,
|
name: nameNode.text,
|
||||||
lineStart: node.startPosition.row + 1,
|
lineStart: node.startPosition.row + 1,
|
||||||
@@ -317,6 +337,7 @@ export class ASTParser {
|
|||||||
isAsync,
|
isAsync,
|
||||||
isExported,
|
isExported,
|
||||||
returnType: returnTypeNode?.text?.replace(/^:\s*/, ""),
|
returnType: returnTypeNode?.text?.replace(/^:\s*/, ""),
|
||||||
|
decorators,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,6 +363,7 @@ export class ASTParser {
|
|||||||
isAsync,
|
isAsync,
|
||||||
isExported,
|
isExported,
|
||||||
returnType: returnTypeNode?.text?.replace(/^:\s*/, ""),
|
returnType: returnTypeNode?.text?.replace(/^:\s*/, ""),
|
||||||
|
decorators: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isExported) {
|
if (isExported) {
|
||||||
@@ -364,7 +386,12 @@ export class ASTParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractClass(node: SyntaxNode, ast: FileAST, isExported: boolean): void {
|
private extractClass(
|
||||||
|
node: SyntaxNode,
|
||||||
|
ast: FileAST,
|
||||||
|
isExported: boolean,
|
||||||
|
externalDecorators: string[] = [],
|
||||||
|
): void {
|
||||||
const nameNode = node.childForFieldName(FieldName.NAME)
|
const nameNode = node.childForFieldName(FieldName.NAME)
|
||||||
if (!nameNode) {
|
if (!nameNode) {
|
||||||
return
|
return
|
||||||
@@ -375,14 +402,19 @@ export class ASTParser {
|
|||||||
const properties: PropertyInfo[] = []
|
const properties: PropertyInfo[] = []
|
||||||
|
|
||||||
if (body) {
|
if (body) {
|
||||||
|
let pendingDecorators: string[] = []
|
||||||
for (const member of body.children) {
|
for (const member of body.children) {
|
||||||
if (member.type === NodeType.METHOD_DEFINITION) {
|
if (member.type === NodeType.DECORATOR) {
|
||||||
methods.push(this.extractMethod(member))
|
pendingDecorators.push(this.formatDecorator(member))
|
||||||
|
} else if (member.type === NodeType.METHOD_DEFINITION) {
|
||||||
|
methods.push(this.extractMethod(member, pendingDecorators))
|
||||||
|
pendingDecorators = []
|
||||||
} else if (
|
} else if (
|
||||||
member.type === NodeType.PUBLIC_FIELD_DEFINITION ||
|
member.type === NodeType.PUBLIC_FIELD_DEFINITION ||
|
||||||
member.type === NodeType.FIELD_DEFINITION
|
member.type === NodeType.FIELD_DEFINITION
|
||||||
) {
|
) {
|
||||||
properties.push(this.extractProperty(member))
|
properties.push(this.extractProperty(member))
|
||||||
|
pendingDecorators = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,6 +422,9 @@ export class ASTParser {
|
|||||||
const { extendsName, implementsList } = this.extractClassHeritage(node)
|
const { extendsName, implementsList } = this.extractClassHeritage(node)
|
||||||
const isAbstract = node.children.some((c) => c.type === NodeType.ABSTRACT)
|
const isAbstract = node.children.some((c) => c.type === NodeType.ABSTRACT)
|
||||||
|
|
||||||
|
const nodeDecorators = this.extractNodeDecorators(node)
|
||||||
|
const decorators = [...externalDecorators, ...nodeDecorators]
|
||||||
|
|
||||||
ast.classes.push({
|
ast.classes.push({
|
||||||
name: nameNode.text,
|
name: nameNode.text,
|
||||||
lineStart: node.startPosition.row + 1,
|
lineStart: node.startPosition.row + 1,
|
||||||
@@ -400,6 +435,7 @@ export class ASTParser {
|
|||||||
implements: implementsList,
|
implements: implementsList,
|
||||||
isExported,
|
isExported,
|
||||||
isAbstract,
|
isAbstract,
|
||||||
|
decorators,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,7 +489,7 @@ export class ASTParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractMethod(node: SyntaxNode): MethodInfo {
|
private extractMethod(node: SyntaxNode, decorators: string[] = []): MethodInfo {
|
||||||
const nameNode = node.childForFieldName(FieldName.NAME)
|
const nameNode = node.childForFieldName(FieldName.NAME)
|
||||||
const params = this.extractParameters(node)
|
const params = this.extractParameters(node)
|
||||||
const isAsync = node.children.some((c) => c.type === NodeType.ASYNC)
|
const isAsync = node.children.some((c) => c.type === NodeType.ASYNC)
|
||||||
@@ -475,6 +511,7 @@ export class ASTParser {
|
|||||||
isAsync,
|
isAsync,
|
||||||
visibility,
|
visibility,
|
||||||
isStatic,
|
isStatic,
|
||||||
|
decorators,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,6 +602,75 @@ export class ASTParser {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extractEnum(node: SyntaxNode, ast: FileAST, isExported: boolean): void {
|
||||||
|
const nameNode = node.childForFieldName(FieldName.NAME)
|
||||||
|
if (!nameNode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = node.childForFieldName(FieldName.BODY)
|
||||||
|
const members: EnumMemberInfo[] = []
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
for (const child of body.children) {
|
||||||
|
if (child.type === NodeType.ENUM_ASSIGNMENT) {
|
||||||
|
const memberName = child.childForFieldName(FieldName.NAME)
|
||||||
|
const memberValue = child.childForFieldName(FieldName.VALUE)
|
||||||
|
if (memberName) {
|
||||||
|
members.push({
|
||||||
|
name: memberName.text,
|
||||||
|
value: this.parseEnumValue(memberValue),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
child.type === NodeType.IDENTIFIER ||
|
||||||
|
child.type === NodeType.PROPERTY_IDENTIFIER
|
||||||
|
) {
|
||||||
|
members.push({
|
||||||
|
name: child.text,
|
||||||
|
value: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isConst = node.children.some((c) => c.text === "const")
|
||||||
|
|
||||||
|
ast.enums.push({
|
||||||
|
name: nameNode.text,
|
||||||
|
lineStart: node.startPosition.row + 1,
|
||||||
|
lineEnd: node.endPosition.row + 1,
|
||||||
|
members,
|
||||||
|
isExported,
|
||||||
|
isConst,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseEnumValue(valueNode: SyntaxNode | null): string | number | undefined {
|
||||||
|
if (!valueNode) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = valueNode.text
|
||||||
|
|
||||||
|
if (valueNode.type === "number") {
|
||||||
|
return Number(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueNode.type === "string") {
|
||||||
|
return this.getStringValue(valueNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueNode.type === "unary_expression" && text.startsWith("-")) {
|
||||||
|
const num = Number(text)
|
||||||
|
if (!isNaN(num)) {
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
private extractParameters(node: SyntaxNode): ParameterInfo[] {
|
private extractParameters(node: SyntaxNode): ParameterInfo[] {
|
||||||
const params: ParameterInfo[] = []
|
const params: ParameterInfo[] = []
|
||||||
const paramsNode = node.childForFieldName(FieldName.PARAMETERS)
|
const paramsNode = node.childForFieldName(FieldName.PARAMETERS)
|
||||||
@@ -613,6 +719,49 @@ export class ASTParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a decorator node to a string like "@Get(':id')" or "@Injectable()".
|
||||||
|
*/
|
||||||
|
private formatDecorator(node: SyntaxNode): string {
|
||||||
|
return node.text.replace(/\s+/g, " ").trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract decorators that are direct children of a node.
|
||||||
|
* In tree-sitter, decorators are children of the class/function declaration.
|
||||||
|
*/
|
||||||
|
private extractNodeDecorators(node: SyntaxNode): string[] {
|
||||||
|
const decorators: string[] = []
|
||||||
|
for (const child of node.children) {
|
||||||
|
if (child.type === NodeType.DECORATOR) {
|
||||||
|
decorators.push(this.formatDecorator(child))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return decorators
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract decorators from sibling nodes before the current node.
|
||||||
|
* Decorators appear as children before the declaration in export statements.
|
||||||
|
*/
|
||||||
|
private extractDecoratorsFromSiblings(node: SyntaxNode): string[] {
|
||||||
|
const decorators: string[] = []
|
||||||
|
const parent = node.parent
|
||||||
|
if (!parent) {
|
||||||
|
return decorators
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sibling of parent.children) {
|
||||||
|
if (sibling.type === NodeType.DECORATOR) {
|
||||||
|
decorators.push(this.formatDecorator(sibling))
|
||||||
|
} else if (sibling === node) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorators
|
||||||
|
}
|
||||||
|
|
||||||
private classifyImport(from: string): ImportInfo["type"] {
|
private classifyImport(from: string): ImportInfo["type"] {
|
||||||
if (from.startsWith(".") || from.startsWith("/")) {
|
if (from.startsWith(".") || from.startsWith("/")) {
|
||||||
return "internal"
|
return "internal"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const NodeType = {
|
|||||||
CLASS_DECLARATION: "class_declaration",
|
CLASS_DECLARATION: "class_declaration",
|
||||||
INTERFACE_DECLARATION: "interface_declaration",
|
INTERFACE_DECLARATION: "interface_declaration",
|
||||||
TYPE_ALIAS_DECLARATION: "type_alias_declaration",
|
TYPE_ALIAS_DECLARATION: "type_alias_declaration",
|
||||||
|
ENUM_DECLARATION: "enum_declaration",
|
||||||
|
|
||||||
// Clauses
|
// Clauses
|
||||||
IMPORT_CLAUSE: "import_clause",
|
IMPORT_CLAUSE: "import_clause",
|
||||||
@@ -37,6 +38,11 @@ export const NodeType = {
|
|||||||
FIELD_DEFINITION: "field_definition",
|
FIELD_DEFINITION: "field_definition",
|
||||||
PROPERTY_SIGNATURE: "property_signature",
|
PROPERTY_SIGNATURE: "property_signature",
|
||||||
|
|
||||||
|
// Enum members
|
||||||
|
ENUM_BODY: "enum_body",
|
||||||
|
ENUM_ASSIGNMENT: "enum_assignment",
|
||||||
|
PROPERTY_IDENTIFIER: "property_identifier",
|
||||||
|
|
||||||
// Parameters
|
// Parameters
|
||||||
REQUIRED_PARAMETER: "required_parameter",
|
REQUIRED_PARAMETER: "required_parameter",
|
||||||
OPTIONAL_PARAMETER: "optional_parameter",
|
OPTIONAL_PARAMETER: "optional_parameter",
|
||||||
@@ -57,6 +63,9 @@ export const NodeType = {
|
|||||||
DEFAULT: "default",
|
DEFAULT: "default",
|
||||||
ACCESSIBILITY_MODIFIER: "accessibility_modifier",
|
ACCESSIBILITY_MODIFIER: "accessibility_modifier",
|
||||||
READONLY: "readonly",
|
READONLY: "readonly",
|
||||||
|
|
||||||
|
// Decorators
|
||||||
|
DECORATOR: "decorator",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type NodeTypeValue = (typeof NodeType)[keyof typeof NodeType]
|
export type NodeTypeValue = (typeof NodeType)[keyof typeof NodeType]
|
||||||
|
|||||||
@@ -187,10 +187,22 @@ function formatFileOverview(
|
|||||||
return lines.join("\n")
|
return lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format decorators as a prefix string.
|
||||||
|
* Example: "@Get(':id') @Auth() "
|
||||||
|
*/
|
||||||
|
function formatDecoratorsPrefix(decorators: string[] | undefined): string {
|
||||||
|
if (!decorators || decorators.length === 0) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return `${decorators.join(" ")} `
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a function signature.
|
* Format a function signature.
|
||||||
*/
|
*/
|
||||||
function formatFunctionSignature(fn: FileAST["functions"][0]): string {
|
function formatFunctionSignature(fn: FileAST["functions"][0]): string {
|
||||||
|
const decoratorsPrefix = formatDecoratorsPrefix(fn.decorators)
|
||||||
const asyncPrefix = fn.isAsync ? "async " : ""
|
const asyncPrefix = fn.isAsync ? "async " : ""
|
||||||
const params = fn.params
|
const params = fn.params
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
@@ -200,7 +212,7 @@ function formatFunctionSignature(fn: FileAST["functions"][0]): string {
|
|||||||
})
|
})
|
||||||
.join(", ")
|
.join(", ")
|
||||||
const returnType = fn.returnType ? `: ${fn.returnType}` : ""
|
const returnType = fn.returnType ? `: ${fn.returnType}` : ""
|
||||||
return `${asyncPrefix}${fn.name}(${params})${returnType}`
|
return `${decoratorsPrefix}${asyncPrefix}${fn.name}(${params})${returnType}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -240,6 +252,37 @@ function formatTypeAliasSignature(type: FileAST["typeAliases"][0]): string {
|
|||||||
return `type ${type.name} = ${definition}`
|
return `type ${type.name} = ${definition}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an enum signature with members and values.
|
||||||
|
* Example: "enum Status { Active=1, Inactive=0, Pending=2 }"
|
||||||
|
* Example: "const enum Role { Admin="admin", User="user" }"
|
||||||
|
*/
|
||||||
|
function formatEnumSignature(enumInfo: FileAST["enums"][0]): string {
|
||||||
|
const constPrefix = enumInfo.isConst ? "const " : ""
|
||||||
|
|
||||||
|
if (enumInfo.members.length === 0) {
|
||||||
|
return `${constPrefix}enum ${enumInfo.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const membersStr = enumInfo.members
|
||||||
|
.map((m) => {
|
||||||
|
if (m.value === undefined) {
|
||||||
|
return m.name
|
||||||
|
}
|
||||||
|
const valueStr = typeof m.value === "string" ? `"${m.value}"` : String(m.value)
|
||||||
|
return `${m.name}=${valueStr}`
|
||||||
|
})
|
||||||
|
.join(", ")
|
||||||
|
|
||||||
|
const result = `${constPrefix}enum ${enumInfo.name} { ${membersStr} }`
|
||||||
|
|
||||||
|
if (result.length > 100) {
|
||||||
|
return truncateDefinition(result, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Truncate long type definitions for display.
|
* Truncate long type definitions for display.
|
||||||
*/
|
*/
|
||||||
@@ -279,9 +322,10 @@ function formatFileSummary(
|
|||||||
|
|
||||||
if (ast.classes.length > 0) {
|
if (ast.classes.length > 0) {
|
||||||
for (const cls of ast.classes) {
|
for (const cls of ast.classes) {
|
||||||
|
const decoratorsPrefix = formatDecoratorsPrefix(cls.decorators)
|
||||||
const ext = cls.extends ? ` extends ${cls.extends}` : ""
|
const ext = cls.extends ? ` extends ${cls.extends}` : ""
|
||||||
const impl = cls.implements.length > 0 ? ` implements ${cls.implements.join(", ")}` : ""
|
const impl = cls.implements.length > 0 ? ` implements ${cls.implements.join(", ")}` : ""
|
||||||
lines.push(`- class ${cls.name}${ext}${impl}`)
|
lines.push(`- ${decoratorsPrefix}class ${cls.name}${ext}${impl}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,6 +341,12 @@ function formatFileSummary(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ast.enums && ast.enums.length > 0) {
|
||||||
|
for (const enumInfo of ast.enums) {
|
||||||
|
lines.push(`- ${formatEnumSignature(enumInfo)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (lines.length === 1) {
|
if (lines.length === 1) {
|
||||||
return `- ${path}${flags}`
|
return `- ${path}${flags}`
|
||||||
}
|
}
|
||||||
@@ -330,6 +380,11 @@ function formatFileSummaryCompact(path: string, ast: FileAST, flags: string): st
|
|||||||
parts.push(`type: ${names}`)
|
parts.push(`type: ${names}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ast.enums && ast.enums.length > 0) {
|
||||||
|
const names = ast.enums.map((e) => e.name).join(", ")
|
||||||
|
parts.push(`enum: ${names}`)
|
||||||
|
}
|
||||||
|
|
||||||
const summary = parts.length > 0 ? ` [${parts.join(" | ")}]` : ""
|
const summary = parts.length > 0 ? ` [${parts.join(" | ")}]` : ""
|
||||||
return `- ${path}${summary}${flags}`
|
return `- ${path}${summary}${flags}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -562,4 +562,274 @@ third: value3`
|
|||||||
expect(ast.exports[2].line).toBe(3)
|
expect(ast.exports[2].line).toBe(3)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("enums (0.24.3)", () => {
|
||||||
|
it("should extract enum with numeric values", () => {
|
||||||
|
const code = `enum Status {
|
||||||
|
Active = 1,
|
||||||
|
Inactive = 0,
|
||||||
|
Pending = 2
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0]).toMatchObject({
|
||||||
|
name: "Status",
|
||||||
|
isExported: false,
|
||||||
|
isConst: false,
|
||||||
|
})
|
||||||
|
expect(ast.enums[0].members).toHaveLength(3)
|
||||||
|
expect(ast.enums[0].members[0]).toMatchObject({ name: "Active", value: 1 })
|
||||||
|
expect(ast.enums[0].members[1]).toMatchObject({ name: "Inactive", value: 0 })
|
||||||
|
expect(ast.enums[0].members[2]).toMatchObject({ name: "Pending", value: 2 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract enum with string values", () => {
|
||||||
|
const code = `enum Role {
|
||||||
|
Admin = "admin",
|
||||||
|
User = "user",
|
||||||
|
Guest = "guest"
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0].members).toHaveLength(3)
|
||||||
|
expect(ast.enums[0].members[0]).toMatchObject({ name: "Admin", value: "admin" })
|
||||||
|
expect(ast.enums[0].members[1]).toMatchObject({ name: "User", value: "user" })
|
||||||
|
expect(ast.enums[0].members[2]).toMatchObject({ name: "Guest", value: "guest" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract enum without explicit values", () => {
|
||||||
|
const code = `enum Direction {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0].members).toHaveLength(4)
|
||||||
|
expect(ast.enums[0].members[0]).toMatchObject({ name: "Up", value: undefined })
|
||||||
|
expect(ast.enums[0].members[1]).toMatchObject({ name: "Down", value: undefined })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract exported enum", () => {
|
||||||
|
const code = `export enum Color {
|
||||||
|
Red = "#FF0000",
|
||||||
|
Green = "#00FF00",
|
||||||
|
Blue = "#0000FF"
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0].isExported).toBe(true)
|
||||||
|
expect(ast.exports).toHaveLength(1)
|
||||||
|
expect(ast.exports[0].kind).toBe("type")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract const enum", () => {
|
||||||
|
const code = `const enum HttpStatus {
|
||||||
|
OK = 200,
|
||||||
|
NotFound = 404,
|
||||||
|
InternalError = 500
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0].isConst).toBe(true)
|
||||||
|
expect(ast.enums[0].members[0]).toMatchObject({ name: "OK", value: 200 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract exported const enum", () => {
|
||||||
|
const code = `export const enum LogLevel {
|
||||||
|
Debug = 0,
|
||||||
|
Info = 1,
|
||||||
|
Warn = 2,
|
||||||
|
Error = 3
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0].isExported).toBe(true)
|
||||||
|
expect(ast.enums[0].isConst).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract line range for enum", () => {
|
||||||
|
const code = `enum Test {
|
||||||
|
A = 1,
|
||||||
|
B = 2
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums[0].lineStart).toBe(1)
|
||||||
|
expect(ast.enums[0].lineEnd).toBe(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle enum with negative values", () => {
|
||||||
|
const code = `enum Temperature {
|
||||||
|
Cold = -10,
|
||||||
|
Freezing = -20,
|
||||||
|
Hot = 40
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0].members[0]).toMatchObject({ name: "Cold", value: -10 })
|
||||||
|
expect(ast.enums[0].members[1]).toMatchObject({ name: "Freezing", value: -20 })
|
||||||
|
expect(ast.enums[0].members[2]).toMatchObject({ name: "Hot", value: 40 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle empty enum", () => {
|
||||||
|
const code = `enum Empty {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(1)
|
||||||
|
expect(ast.enums[0].name).toBe("Empty")
|
||||||
|
expect(ast.enums[0].members).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not extract enum from JavaScript", () => {
|
||||||
|
const code = `enum Status { Active = 1 }`
|
||||||
|
const ast = parser.parse(code, "js")
|
||||||
|
|
||||||
|
expect(ast.enums).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("decorators (0.24.4)", () => {
|
||||||
|
it("should extract class decorator", () => {
|
||||||
|
const code = `@Controller('users')
|
||||||
|
class UserController {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators[0]).toBe("@Controller('users')")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract multiple class decorators", () => {
|
||||||
|
const code = `@Controller('api')
|
||||||
|
@Injectable()
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
class ApiController {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators).toHaveLength(3)
|
||||||
|
expect(ast.classes[0].decorators[0]).toBe("@Controller('api')")
|
||||||
|
expect(ast.classes[0].decorators[1]).toBe("@Injectable()")
|
||||||
|
expect(ast.classes[0].decorators[2]).toBe("@UseGuards(AuthGuard)")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract method decorators", () => {
|
||||||
|
const code = `class UserController {
|
||||||
|
@Get(':id')
|
||||||
|
@Auth()
|
||||||
|
async getUser() {}
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].methods).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].methods[0].decorators).toHaveLength(2)
|
||||||
|
expect(ast.classes[0].methods[0].decorators[0]).toBe("@Get(':id')")
|
||||||
|
expect(ast.classes[0].methods[0].decorators[1]).toBe("@Auth()")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract exported decorated class", () => {
|
||||||
|
const code = `@Injectable()
|
||||||
|
export class UserService {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].isExported).toBe(true)
|
||||||
|
expect(ast.classes[0].decorators).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators[0]).toBe("@Injectable()")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract decorator with complex arguments", () => {
|
||||||
|
const code = `@Module({
|
||||||
|
imports: [UserModule],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService]
|
||||||
|
})
|
||||||
|
class AppModule {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators[0]).toContain("@Module")
|
||||||
|
expect(ast.classes[0].decorators[0]).toContain("imports")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract decorated class with extends", () => {
|
||||||
|
const code = `@Entity()
|
||||||
|
class User extends BaseEntity {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].extends).toBe("BaseEntity")
|
||||||
|
expect(ast.classes[0].decorators).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators![0]).toBe("@Entity()")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle class without decorators", () => {
|
||||||
|
const code = `class SimpleClass {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle method without decorators", () => {
|
||||||
|
const code = `class SimpleClass {
|
||||||
|
simpleMethod() {}
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].methods).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].methods[0].decorators).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle function without decorators", () => {
|
||||||
|
const code = `function simpleFunc() {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.functions).toHaveLength(1)
|
||||||
|
expect(ast.functions[0].decorators).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle arrow function without decorators", () => {
|
||||||
|
const code = `const arrowFn = () => {}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.functions).toHaveLength(1)
|
||||||
|
expect(ast.functions[0].decorators).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should extract NestJS controller pattern", () => {
|
||||||
|
const code = `@Controller('users')
|
||||||
|
export class UserController {
|
||||||
|
@Get()
|
||||||
|
findAll() {}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
findOne() {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Body()
|
||||||
|
create() {}
|
||||||
|
}`
|
||||||
|
const ast = parser.parse(code, "ts")
|
||||||
|
|
||||||
|
expect(ast.classes).toHaveLength(1)
|
||||||
|
expect(ast.classes[0].decorators).toContain("@Controller('users')")
|
||||||
|
expect(ast.classes[0].methods).toHaveLength(3)
|
||||||
|
expect(ast.classes[0].methods[0].decorators).toContain("@Get()")
|
||||||
|
expect(ast.classes[0].methods[1].decorators).toContain("@Get(':id')")
|
||||||
|
expect(ast.classes[0].methods[2].decorators).toContain("@Post()")
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1191,9 +1191,7 @@ describe("prompts", () => {
|
|||||||
|
|
||||||
const context = buildInitialContext(structure, asts)
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
expect(context).toContain(
|
expect(context).toContain("interface AdminUser extends User { role: string }")
|
||||||
"interface AdminUser extends User { role: string }",
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should format interface with readonly fields", () => {
|
it("should format interface with readonly fields", () => {
|
||||||
@@ -1494,4 +1492,525 @@ describe("prompts", () => {
|
|||||||
expect(context).toContain("- type Handler = (event: Event) => void")
|
expect(context).toContain("- type Handler = (event: Event) => void")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("enum definitions (0.24.3)", () => {
|
||||||
|
it("should format enum with numeric values", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["enums.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"enums.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
enums: [
|
||||||
|
{
|
||||||
|
name: "Status",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 5,
|
||||||
|
members: [
|
||||||
|
{ name: "Active", value: 1 },
|
||||||
|
{ name: "Inactive", value: 0 },
|
||||||
|
{ name: "Pending", value: 2 },
|
||||||
|
],
|
||||||
|
isExported: true,
|
||||||
|
isConst: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- enum Status { Active=1, Inactive=0, Pending=2 }")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format enum with string values", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["enums.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"enums.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
enums: [
|
||||||
|
{
|
||||||
|
name: "Role",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 5,
|
||||||
|
members: [
|
||||||
|
{ name: "Admin", value: "admin" },
|
||||||
|
{ name: "User", value: "user" },
|
||||||
|
],
|
||||||
|
isExported: true,
|
||||||
|
isConst: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain('- enum Role { Admin="admin", User="user" }')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format const enum", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["enums.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"enums.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
enums: [
|
||||||
|
{
|
||||||
|
name: "HttpStatus",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 5,
|
||||||
|
members: [
|
||||||
|
{ name: "OK", value: 200 },
|
||||||
|
{ name: "NotFound", value: 404 },
|
||||||
|
],
|
||||||
|
isExported: true,
|
||||||
|
isConst: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- const enum HttpStatus { OK=200, NotFound=404 }")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format enum without explicit values", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["enums.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"enums.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
enums: [
|
||||||
|
{
|
||||||
|
name: "Direction",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 5,
|
||||||
|
members: [
|
||||||
|
{ name: "Up", value: undefined },
|
||||||
|
{ name: "Down", value: undefined },
|
||||||
|
],
|
||||||
|
isExported: true,
|
||||||
|
isConst: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- enum Direction { Up, Down }")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format empty enum", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["enums.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"enums.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
enums: [
|
||||||
|
{
|
||||||
|
name: "Empty",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 1,
|
||||||
|
members: [],
|
||||||
|
isExported: true,
|
||||||
|
isConst: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- enum Empty")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should include enum in compact format", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["enums.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"enums.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
enums: [
|
||||||
|
{
|
||||||
|
name: "Status",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 5,
|
||||||
|
members: [{ name: "Active", value: 1 }],
|
||||||
|
isExported: true,
|
||||||
|
isConst: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts, undefined, {
|
||||||
|
includeSignatures: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(context).toContain("enum: Status")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should truncate long enum definitions", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["enums.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"enums.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
enums: [
|
||||||
|
{
|
||||||
|
name: "VeryLongEnumName",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 20,
|
||||||
|
members: [
|
||||||
|
{ name: "FirstVeryLongMemberName", value: "first_value" },
|
||||||
|
{ name: "SecondVeryLongMemberName", value: "second_value" },
|
||||||
|
{ name: "ThirdVeryLongMemberName", value: "third_value" },
|
||||||
|
],
|
||||||
|
isExported: true,
|
||||||
|
isConst: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("...")
|
||||||
|
expect(context).toContain("- enum VeryLongEnumName")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("decorators (0.24.4)", () => {
|
||||||
|
it("should format function with decorators", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["controller.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"controller.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [
|
||||||
|
{
|
||||||
|
name: "getUser",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 5,
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
hasDefault: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isAsync: true,
|
||||||
|
isExported: true,
|
||||||
|
returnType: "Promise<User>",
|
||||||
|
decorators: ["@Get(':id')"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- @Get(':id') async getUser(id: string): Promise<User>")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format class with decorators", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["controller.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"controller.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [
|
||||||
|
{
|
||||||
|
name: "UserController",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 20,
|
||||||
|
methods: [],
|
||||||
|
properties: [],
|
||||||
|
implements: [],
|
||||||
|
isExported: true,
|
||||||
|
isAbstract: false,
|
||||||
|
decorators: ["@Controller('users')"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- @Controller('users') class UserController")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format class with multiple decorators", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["service.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"service.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [
|
||||||
|
{
|
||||||
|
name: "UserService",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 30,
|
||||||
|
methods: [],
|
||||||
|
properties: [],
|
||||||
|
implements: ["IUserService"],
|
||||||
|
isExported: true,
|
||||||
|
isAbstract: false,
|
||||||
|
decorators: ["@Injectable()", "@Singleton()"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain(
|
||||||
|
"- @Injectable() @Singleton() class UserService implements IUserService",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should format function with multiple decorators", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["controller.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"controller.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [
|
||||||
|
{
|
||||||
|
name: "createUser",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 10,
|
||||||
|
params: [],
|
||||||
|
isAsync: true,
|
||||||
|
isExported: true,
|
||||||
|
decorators: ["@Post()", "@Auth()", "@ValidateBody()"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- @Post() @Auth() @ValidateBody() async createUser()")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle function without decorators", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["utils.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"utils.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [
|
||||||
|
{
|
||||||
|
name: "helper",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 5,
|
||||||
|
params: [],
|
||||||
|
isAsync: false,
|
||||||
|
isExported: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
classes: [],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- helper()")
|
||||||
|
expect(context).not.toContain("@")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle class without decorators", () => {
|
||||||
|
const structure: ProjectStructure = {
|
||||||
|
name: "test",
|
||||||
|
rootPath: "/test",
|
||||||
|
files: ["simple.ts"],
|
||||||
|
directories: [],
|
||||||
|
}
|
||||||
|
const asts = new Map<string, FileAST>([
|
||||||
|
[
|
||||||
|
"simple.ts",
|
||||||
|
{
|
||||||
|
imports: [],
|
||||||
|
exports: [],
|
||||||
|
functions: [],
|
||||||
|
classes: [
|
||||||
|
{
|
||||||
|
name: "SimpleClass",
|
||||||
|
lineStart: 1,
|
||||||
|
lineEnd: 10,
|
||||||
|
methods: [],
|
||||||
|
properties: [],
|
||||||
|
implements: [],
|
||||||
|
isExported: true,
|
||||||
|
isAbstract: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
interfaces: [],
|
||||||
|
typeAliases: [],
|
||||||
|
parseError: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
const context = buildInitialContext(structure, asts)
|
||||||
|
|
||||||
|
expect(context).toContain("- class SimpleClass")
|
||||||
|
expect(context).not.toContain("@")
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user