/**
* Angular Analyzer - Comprehensive Angular-specific code analysis
* Understands components, services, directives, pipes, modules, guards, interceptors, etc.
* Detects state management patterns, architectural layers, and Angular-specific patterns
*/
import { promises as fs } from "fs";
import path from "path";
import {
FrameworkAnalyzer,
AnalysisResult,
CodebaseMetadata,
CodeChunk,
CodeComponent,
ImportStatement,
ExportStatement,
Dependency,
FrameworkInfo,
ArchitecturalLayer,
} from "../../types/index.js";
import { parse } from "@typescript-eslint/typescript-estree";
import { createChunksFromCode } from "../../utils/chunking.js";
export class AngularAnalyzer implements FrameworkAnalyzer {
readonly name = "angular";
readonly version = "1.0.0";
readonly supportedExtensions = [
".ts",
".js",
".html",
".scss",
".css",
".sass",
".less",
];
readonly priority = 100; // Highest priority for Angular files
private angularPatterns = {
component: /@Component\s*\(/,
service: /@Injectable\s*\(/,
directive: /@Directive\s*\(/,
pipe: /@Pipe\s*\(/,
module: /@NgModule\s*\(/,
// Guards: Check for interface implementation OR method signature OR functional guard
guard:
/(?:implements\s+(?:CanActivate|CanDeactivate|CanLoad|CanMatch)|canActivate\s*\(|canDeactivate\s*\(|canLoad\s*\(|canMatch\s*\(|CanActivateFn|CanDeactivateFn|CanMatchFn)/,
interceptor:
/(?:implements\s+HttpInterceptor|intercept\s*\(|HttpInterceptorFn)/,
resolver: /(?:implements\s+Resolve|resolve\s*\(|ResolveFn)/,
validator: /(?:implements\s+(?:Validator|AsyncValidator)|validate\s*\()/,
};
private stateManagementPatterns = {
ngrx: /@ngrx\/store|createAction|createReducer|createSelector/,
akita: /@datorama\/akita|Query|Store\.update/,
elf: /@ngneat\/elf|createStore|withEntities/,
signals:
/\bsignal\s*[<(]|\bcomputed\s*[<(]|\beffect\s*\(|\blinkedSignal\s*[<(]/,
rxjsState: /BehaviorSubject|ReplaySubject|shareReplay/,
};
private modernAngularPatterns = {
signalInput: /\binput\s*[<(]|\binput\.required\s*[<(]/,
signalOutput: /\boutput\s*[<(]/,
signalModel: /\bmodel\s*[<(]|\bmodel\.required\s*[<(]/,
signalViewChild: /\bviewChild\s*[<(]|\bviewChild\.required\s*[<(]/,
signalViewChildren: /\bviewChildren\s*[<(]/,
signalContentChild: /\bcontentChild\s*[<(]|\bcontentChild\.required\s*[<(]/,
signalContentChildren: /\bcontentChildren\s*[<(]/,
controlFlowIf: /@if\s*\(/,
controlFlowFor: /@for\s*\(/,
controlFlowSwitch: /@switch\s*\(/,
controlFlowDefer: /@defer\s*[({]/,
injectFunction: /\binject\s*[<(]/,
};
canAnalyze(filePath: string, content?: string): boolean {
const ext = path.extname(filePath).toLowerCase();
if (!this.supportedExtensions.includes(ext)) {
return false;
}
// For TypeScript files, check if it contains Angular decorators
if (ext === ".ts" && content) {
return Object.values(this.angularPatterns).some((pattern) =>
pattern.test(content)
);
}
// Angular component templates and styles
if ([".html", ".scss", ".css", ".sass", ".less"].includes(ext)) {
// Check if there's a corresponding .ts file
const baseName = filePath.replace(/\.(html|scss|css|sass|less)$/, "");
return true; // We'll verify during analysis
}
return false;
}
async analyze(filePath: string, content: string): Promise<AnalysisResult> {
const ext = path.extname(filePath).toLowerCase();
const relativePath = path.relative(process.cwd(), filePath);
if (ext === ".ts") {
return this.analyzeTypeScriptFile(filePath, content, relativePath);
} else if (ext === ".html") {
return this.analyzeTemplateFile(filePath, content, relativePath);
} else if ([".scss", ".css", ".sass", ".less"].includes(ext)) {
return this.analyzeStyleFile(filePath, content, relativePath);
}
// Fallback
return {
filePath,
language: "unknown",
framework: "angular",
components: [],
imports: [],
exports: [],
dependencies: [],
metadata: {},
chunks: [],
};
}
private async analyzeTypeScriptFile(
filePath: string,
content: string,
relativePath: string
): Promise<AnalysisResult> {
const components: CodeComponent[] = [];
const imports: ImportStatement[] = [];
const exports: ExportStatement[] = [];
const dependencies: string[] = [];
try {
const ast = parse(content, {
loc: true,
range: true,
comment: true,
});
// Extract imports
for (const node of ast.body) {
if (node.type === "ImportDeclaration" && node.source.value) {
const source = node.source.value as string;
imports.push({
source,
imports: node.specifiers.map((s: any) => {
if (s.type === "ImportDefaultSpecifier") return "default";
if (s.type === "ImportNamespaceSpecifier") return "*";
return s.imported?.name || s.local.name;
}),
isDefault: node.specifiers.some(
(s: any) => s.type === "ImportDefaultSpecifier"
),
isDynamic: false,
line: node.loc?.start.line,
});
// Track dependencies
if (!source.startsWith(".") && !source.startsWith("/")) {
dependencies.push(source.split("/")[0]);
}
}
// Extract class declarations with decorators
if (
node.type === "ExportNamedDeclaration" &&
node.declaration?.type === "ClassDeclaration"
) {
const classNode = node.declaration;
if (classNode.id && classNode.decorators) {
const component = await this.extractAngularComponent(
classNode,
content
);
if (component) {
components.push(component);
}
}
}
// Handle direct class exports
if (node.type === "ClassDeclaration" && node.id && node.decorators) {
const component = await this.extractAngularComponent(node, content);
if (component) {
components.push(component);
}
}
// Extract exports
if (node.type === "ExportNamedDeclaration") {
if (node.declaration) {
if (
node.declaration.type === "ClassDeclaration" &&
node.declaration.id
) {
exports.push({
name: node.declaration.id.name,
isDefault: false,
type: "class",
});
}
}
}
if (node.type === "ExportDefaultDeclaration") {
const name =
node.declaration.type === "Identifier"
? node.declaration.name
: "default";
exports.push({
name,
isDefault: true,
type: "default",
});
}
}
} catch (error) {
console.warn(
`Failed to parse Angular TypeScript file ${filePath}:`,
error
);
}
// Detect state management
const statePattern = this.detectStateManagement(content);
// Detect Angular v17+ modern patterns
const modernPatterns = this.detectModernAngularPatterns(content);
// Determine architectural layer
const layer = this.determineLayer(filePath, components);
// Create chunks with Angular-specific metadata
const chunks = await createChunksFromCode(
content,
filePath,
relativePath,
"typescript",
components,
{
framework: "angular",
layer,
statePattern,
dependencies,
modernPatterns,
}
);
// Build detected patterns for the indexer to forward
const detectedPatterns: Array<{ category: string; name: string }> = [];
// Dependency Injection pattern
if (modernPatterns.includes("injectFunction")) {
detectedPatterns.push({ category: "dependencyInjection", name: "inject() function" });
} else if (content.includes("constructor(") && content.includes("private") &&
(relativePath.endsWith(".service.ts") || relativePath.endsWith(".component.ts"))) {
detectedPatterns.push({ category: "dependencyInjection", name: "Constructor injection" });
}
// State Management pattern
if (/BehaviorSubject|ReplaySubject|Subject|Observable/.test(content)) {
detectedPatterns.push({ category: "stateManagement", name: "RxJS" });
}
if (modernPatterns.some((p) => p.startsWith("signal"))) {
detectedPatterns.push({ category: "stateManagement", name: "Signals" });
}
// Reactivity patterns
if (/\beffect\s*\(/.test(content)) {
detectedPatterns.push({ category: "reactivity", name: "Effect" });
}
if (/\bcomputed\s*[<(]/.test(content)) {
detectedPatterns.push({ category: "reactivity", name: "Computed" });
}
// Component Style pattern detection
// Logic: explicit standalone: true → Standalone
// explicit standalone: false → NgModule-based
// no explicit flag + uses modern patterns (inject, signals) → likely Standalone (Angular v19+ default)
// no explicit flag + no modern patterns → ambiguous, don't classify
const hasExplicitStandalone = content.includes("standalone: true");
const hasExplicitNgModule = content.includes("standalone: false");
const usesModernPatterns = modernPatterns.includes("injectFunction") ||
modernPatterns.some(p => p.startsWith("signal"));
if (relativePath.endsWith("component.ts") || relativePath.endsWith("directive.ts") || relativePath.endsWith("pipe.ts")) {
if (hasExplicitStandalone) {
detectedPatterns.push({ category: "componentStyle", name: "Standalone" });
} else if (hasExplicitNgModule) {
detectedPatterns.push({ category: "componentStyle", name: "NgModule-based" });
} else if (usesModernPatterns) {
// No explicit flag but uses modern patterns → likely v19+ standalone default
detectedPatterns.push({ category: "componentStyle", name: "Standalone" });
}
// If no explicit flag and no modern patterns, don't classify (ambiguous)
}
// Input style pattern
if (modernPatterns.includes("signalInput")) {
detectedPatterns.push({ category: "componentInputs", name: "Signal-based inputs" });
} else if (content.includes("@Input()")) {
detectedPatterns.push({ category: "componentInputs", name: "Decorator-based @Input" });
}
return {
filePath,
language: "typescript",
framework: "angular",
components,
imports,
exports,
dependencies: dependencies.map((name) => ({
name,
category: this.categorizeDependency(name),
layer,
})),
metadata: {
analyzer: this.name,
layer,
statePattern,
modernPatterns,
// isStandalone: true if explicit standalone: true, or if uses modern patterns (implying v19+ default)
isStandalone: content.includes("standalone: true") ||
(!content.includes("standalone: false") &&
(modernPatterns.includes("injectFunction") || modernPatterns.some(p => p.startsWith("signal")))),
hasRoutes:
content.includes("RouterModule") || content.includes("routes"),
usesSignals:
modernPatterns.length > 0 &&
modernPatterns.some((p) => p.startsWith("signal")),
usesControlFlow: modernPatterns.some((p) =>
p.startsWith("controlFlow")
),
usesInject: modernPatterns.includes("injectFunction"),
usesRxJS: /BehaviorSubject|ReplaySubject|Subject|Observable/.test(content),
usesEffect: /\beffect\s*\(/.test(content),
usesComputed: /\bcomputed\s*[<(]/.test(content),
componentType: components.length > 0 ? components[0].metadata.angularType : undefined,
// NEW: Patterns for the indexer to forward generically
detectedPatterns,
},
chunks,
};
}
/**
* Detect Angular v17+ modern patterns in the code
*/
private detectModernAngularPatterns(content: string): string[] {
const detected: string[] = [];
for (const [patternName, regex] of Object.entries(
this.modernAngularPatterns
)) {
if (regex.test(content)) {
detected.push(patternName);
}
}
return detected;
}
private async extractAngularComponent(
classNode: any,
content: string
): Promise<CodeComponent | null> {
if (!classNode.decorators || classNode.decorators.length === 0) {
return null;
}
const decorator = classNode.decorators[0];
const decoratorName =
decorator.expression.callee?.name || decorator.expression.name;
let componentType: string | undefined;
let angularType: string | undefined;
// Determine Angular component type
if (decoratorName === "Component") {
componentType = "component";
angularType = "component";
} else if (decoratorName === "Directive") {
componentType = "directive";
angularType = "directive";
} else if (decoratorName === "Pipe") {
componentType = "pipe";
angularType = "pipe";
} else if (decoratorName === "NgModule") {
componentType = "module";
angularType = "module";
} else if (decoratorName === "Injectable") {
// For @Injectable, check if it's actually a guard/interceptor/resolver/validator
// before defaulting to 'service'
const classContent = content.substring(
classNode.range[0],
classNode.range[1]
);
if (this.angularPatterns.guard.test(classContent)) {
componentType = "guard";
angularType = "guard";
} else if (this.angularPatterns.interceptor.test(classContent)) {
componentType = "interceptor";
angularType = "interceptor";
} else if (this.angularPatterns.resolver.test(classContent)) {
componentType = "resolver";
angularType = "resolver";
} else if (this.angularPatterns.validator.test(classContent)) {
componentType = "validator";
angularType = "validator";
} else {
// Default to service if no specific pattern matches
componentType = "service";
angularType = "service";
}
}
// If still no type, check patterns one more time (for classes without decorators)
if (!componentType) {
const classContent = content.substring(
classNode.range[0],
classNode.range[1]
);
if (this.angularPatterns.guard.test(classContent)) {
componentType = "guard";
angularType = "guard";
} else if (this.angularPatterns.interceptor.test(classContent)) {
componentType = "interceptor";
angularType = "interceptor";
} else if (this.angularPatterns.resolver.test(classContent)) {
componentType = "resolver";
angularType = "resolver";
} else if (this.angularPatterns.validator.test(classContent)) {
componentType = "validator";
angularType = "validator";
}
}
// Extract decorator metadata
const decoratorMetadata = this.extractDecoratorMetadata(decorator);
// Extract lifecycle hooks
const lifecycle = this.extractLifecycleHooks(classNode);
// Extract injected dependencies
const injectedServices = this.extractInjectedServices(classNode);
// Extract inputs and outputs
const inputs = this.extractInputs(classNode);
const outputs = this.extractOutputs(classNode);
return {
name: classNode.id.name,
type: "class",
componentType,
startLine: classNode.loc.start.line,
endLine: classNode.loc.end.line,
decorators: [
{
name: decoratorName,
properties: decoratorMetadata,
},
],
lifecycle,
dependencies: injectedServices,
properties: [...inputs, ...outputs],
metadata: {
angularType,
selector: decoratorMetadata.selector,
isStandalone: decoratorMetadata.standalone === true,
template: decoratorMetadata.template,
templateUrl: decoratorMetadata.templateUrl,
styleUrls: decoratorMetadata.styleUrls,
inputs: inputs.map((i) => i.name),
outputs: outputs.map((o) => o.name),
},
};
}
private extractDecoratorMetadata(decorator: any): Record<string, any> {
const metadata: Record<string, any> = {};
try {
if (decorator.expression.arguments && decorator.expression.arguments[0]) {
const arg = decorator.expression.arguments[0];
if (arg.type === "ObjectExpression") {
for (const prop of arg.properties) {
if (prop.key && prop.value) {
const key = prop.key.name || prop.key.value;
if (prop.value.type === "Literal") {
metadata[key] = prop.value.value;
} else if (prop.value.type === "ArrayExpression") {
metadata[key] = prop.value.elements
.map((el: any) => (el.type === "Literal" ? el.value : null))
.filter(Boolean);
} else if (prop.value.type === "Identifier") {
metadata[key] = prop.value.name;
}
}
}
}
}
} catch (error) {
console.warn("Failed to extract decorator metadata:", error);
}
return metadata;
}
private extractLifecycleHooks(classNode: any): string[] {
const hooks: string[] = [];
const lifecycleHooks = [
"ngOnChanges",
"ngOnInit",
"ngDoCheck",
"ngAfterContentInit",
"ngAfterContentChecked",
"ngAfterViewInit",
"ngAfterViewChecked",
"ngOnDestroy",
];
if (classNode.body && classNode.body.body) {
for (const member of classNode.body.body) {
if (member.type === "MethodDefinition" && member.key) {
const methodName = member.key.name;
if (lifecycleHooks.includes(methodName)) {
hooks.push(methodName);
}
}
}
}
return hooks;
}
private extractInjectedServices(classNode: any): string[] {
const services: string[] = [];
// Look for constructor parameters
if (classNode.body && classNode.body.body) {
for (const member of classNode.body.body) {
if (
member.type === "MethodDefinition" &&
member.kind === "constructor"
) {
if (member.value.params) {
for (const param of member.value.params) {
if (param.typeAnnotation?.typeAnnotation?.typeName) {
services.push(
param.typeAnnotation.typeAnnotation.typeName.name
);
}
}
}
}
}
}
return services;
}
private extractInputs(classNode: any): any[] {
const inputs: any[] = [];
if (classNode.body && classNode.body.body) {
for (const member of classNode.body.body) {
if (member.type === "PropertyDefinition") {
// Check for decorator-based @Input()
if (member.decorators) {
const hasInput = member.decorators.some(
(d: any) =>
d.expression?.callee?.name === "Input" ||
d.expression?.name === "Input"
);
if (hasInput && member.key) {
inputs.push({
name: member.key.name,
type: member.typeAnnotation?.typeAnnotation?.type || "any",
style: "decorator",
});
}
}
// Check for signal-based input() (Angular v17.1+)
if (member.value && member.key) {
const valueStr =
member.value.type === "CallExpression"
? member.value.callee?.name || member.value.callee?.object?.name
: null;
if (valueStr === "input") {
inputs.push({
name: member.key.name,
type: "InputSignal",
style: "signal",
required: member.value.callee?.property?.name === "required",
});
}
}
}
}
}
return inputs;
}
private extractOutputs(classNode: any): any[] {
const outputs: any[] = [];
if (classNode.body && classNode.body.body) {
for (const member of classNode.body.body) {
if (member.type === "PropertyDefinition") {
// Check for decorator-based @Output()
if (member.decorators) {
const hasOutput = member.decorators.some(
(d: any) =>
d.expression?.callee?.name === "Output" ||
d.expression?.name === "Output"
);
if (hasOutput && member.key) {
outputs.push({
name: member.key.name,
type: "EventEmitter",
style: "decorator",
});
}
}
// Check for signal-based output() (Angular v17.1+)
if (member.value && member.key) {
const valueStr =
member.value.type === "CallExpression"
? member.value.callee?.name
: null;
if (valueStr === "output") {
outputs.push({
name: member.key.name,
type: "OutputEmitterRef",
style: "signal",
});
}
}
}
}
}
return outputs;
}
private async analyzeTemplateFile(
filePath: string,
content: string,
relativePath: string
): Promise<AnalysisResult> {
// Find corresponding component file
const componentPath = filePath.replace(/\.html$/, ".ts");
// Detect legacy vs modern control flow
const hasLegacyDirectives = /\*ng(?:If|For|Switch)/.test(content);
const hasModernControlFlow = /@(?:if|for|switch|defer)\s*[({]/.test(
content
);
return {
filePath,
language: "html",
framework: "angular",
components: [],
imports: [],
exports: [],
dependencies: [],
metadata: {
analyzer: this.name,
type: "template",
componentPath,
hasLegacyDirectives,
hasModernControlFlow,
hasBindings: /\[|\(|{{/.test(content),
hasDefer: /@defer\s*[({]/.test(content),
},
chunks: await createChunksFromCode(
content,
filePath,
relativePath,
"html",
[]
),
};
}
private async analyzeStyleFile(
filePath: string,
content: string,
relativePath: string
): Promise<AnalysisResult> {
const ext = path.extname(filePath).toLowerCase();
const language = ext.substring(1); // Remove the dot
return {
filePath,
language,
framework: "angular",
components: [],
imports: [],
exports: [],
dependencies: [],
metadata: {
analyzer: this.name,
type: "style",
},
chunks: await createChunksFromCode(
content,
filePath,
relativePath,
language,
[]
),
};
}
private detectStateManagement(content: string): string | undefined {
for (const [pattern, regex] of Object.entries(
this.stateManagementPatterns
)) {
if (regex.test(content)) {
return pattern;
}
}
return undefined;
}
private determineLayer(
filePath: string,
components: CodeComponent[]
): ArchitecturalLayer {
const lowerPath = filePath.toLowerCase();
// Check path-based patterns
if (
lowerPath.includes("/component") ||
lowerPath.includes("/view") ||
lowerPath.includes("/page")
) {
return "presentation";
}
if (lowerPath.includes("/service")) {
return "business";
}
if (
lowerPath.includes("/data") ||
lowerPath.includes("/repository") ||
lowerPath.includes("/api")
) {
return "data";
}
if (
lowerPath.includes("/store") ||
lowerPath.includes("/state") ||
lowerPath.includes("/ngrx")
) {
return "state";
}
if (lowerPath.includes("/core")) {
return "core";
}
if (lowerPath.includes("/shared")) {
return "shared";
}
if (lowerPath.includes("/feature")) {
return "feature";
}
// Check component types
for (const component of components) {
if (
component.componentType === "component" ||
component.componentType === "directive" ||
component.componentType === "pipe"
) {
return "presentation";
}
if (component.componentType === "service") {
return lowerPath.includes("http") || lowerPath.includes("api")
? "data"
: "business";
}
if (
component.componentType === "guard" ||
component.componentType === "interceptor"
) {
return "core";
}
}
return "unknown";
}
private categorizeDependency(name: string): any {
if (name.startsWith("@angular/")) {
return "framework";
}
if (
name.includes("ngrx") ||
name.includes("akita") ||
name.includes("elf")
) {
return "state";
}
if (
name.includes("material") ||
name.includes("primeng") ||
name.includes("ng-bootstrap")
) {
return "ui";
}
if (name.includes("router")) {
return "routing";
}
if (name.includes("http") || name.includes("common/http")) {
return "http";
}
if (
name.includes("test") ||
name.includes("jest") ||
name.includes("jasmine") ||
name.includes("karma")
) {
return "testing";
}
return "other";
}
async detectCodebaseMetadata(rootPath: string): Promise<CodebaseMetadata> {
const metadata: CodebaseMetadata = {
name: path.basename(rootPath),
rootPath,
languages: [],
dependencies: [],
architecture: {
type: "feature-based",
layers: {
presentation: 0,
business: 0,
data: 0,
state: 0,
core: 0,
shared: 0,
feature: 0,
infrastructure: 0,
unknown: 0,
},
patterns: [],
},
styleGuides: [],
documentation: [],
projectStructure: {
type: "single-app",
},
statistics: {
totalFiles: 0,
totalLines: 0,
totalComponents: 0,
componentsByType: {},
componentsByLayer: {
presentation: 0,
business: 0,
data: 0,
state: 0,
core: 0,
shared: 0,
feature: 0,
infrastructure: 0,
unknown: 0,
},
},
customMetadata: {},
};
try {
// Read package.json
const packageJsonPath = path.join(rootPath, "package.json");
const packageJson = JSON.parse(
await fs.readFile(packageJsonPath, "utf-8")
);
metadata.name = packageJson.name || metadata.name;
// Extract Angular version and dependencies
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
const angularVersion =
allDeps["@angular/core"]?.replace(/[\^~]/, "") || "unknown";
// Detect state management
const stateManagement: string[] = [];
if (allDeps["@ngrx/store"]) stateManagement.push("ngrx");
if (allDeps["@datorama/akita"]) stateManagement.push("akita");
if (allDeps["@ngneat/elf"]) stateManagement.push("elf");
// Detect UI libraries
const uiLibraries: string[] = [];
if (allDeps["@angular/material"]) uiLibraries.push("Angular Material");
if (allDeps["primeng"]) uiLibraries.push("PrimeNG");
if (allDeps["@ng-bootstrap/ng-bootstrap"])
uiLibraries.push("ng-bootstrap");
// Detect testing frameworks
const testingFrameworks: string[] = [];
if (allDeps["jasmine-core"]) testingFrameworks.push("Jasmine");
if (allDeps["karma"]) testingFrameworks.push("Karma");
if (allDeps["jest"]) testingFrameworks.push("Jest");
metadata.framework = {
name: "Angular",
version: angularVersion,
type: "angular",
variant: "unknown", // Will be determined during analysis
stateManagement,
uiLibraries,
testingFrameworks,
};
// Convert dependencies
metadata.dependencies = Object.entries(allDeps).map(
([name, version]) => ({
name,
version: version as string,
category: this.categorizeDependency(name),
})
);
} catch (error) {
console.warn("Failed to read Angular project metadata:", error);
}
// Calculate statistics from existing index if available
try {
const indexPath = path.join(rootPath, ".codebase-index.json");
const indexContent = await fs.readFile(indexPath, "utf-8");
const chunks = JSON.parse(indexContent);
console.error(
`Loading statistics from ${indexPath}: ${chunks.length} chunks`
);
if (Array.isArray(chunks) && chunks.length > 0) {
metadata.statistics.totalFiles = new Set(
chunks.map((c: any) => c.filePath)
).size;
metadata.statistics.totalLines = chunks.reduce(
(sum: number, c: any) => sum + (c.endLine - c.startLine + 1),
0
);
// Count components by type
const componentCounts: Record<string, number> = {};
const layerCounts: Record<string, number> = {
presentation: 0,
business: 0,
data: 0,
state: 0,
core: 0,
shared: 0,
feature: 0,
infrastructure: 0,
unknown: 0,
};
for (const chunk of chunks) {
if (chunk.componentType) {
componentCounts[chunk.componentType] =
(componentCounts[chunk.componentType] || 0) + 1;
metadata.statistics.totalComponents++;
}
if (chunk.layer) {
layerCounts[chunk.layer as keyof typeof layerCounts] =
(layerCounts[chunk.layer as keyof typeof layerCounts] || 0) + 1;
}
}
metadata.statistics.componentsByType = componentCounts;
metadata.statistics.componentsByLayer = layerCounts;
metadata.architecture.layers = layerCounts;
}
} catch (error) {
// Index doesn't exist yet, keep statistics at 0
console.warn("Failed to calculate statistics from index:", error);
}
return metadata;
}
/**
* Generate Angular-specific summary for a code chunk
*/
summarize(chunk: CodeChunk): string {
const { componentType, metadata, content } = chunk;
const fileName = path.basename(chunk.filePath);
// Extract class/component name
const classMatch = content.match(/(?:export\s+)?class\s+(\w+)/);
const className = classMatch ? classMatch[1] : fileName;
switch (componentType) {
case "component":
const selector = metadata.decorator?.selector || "unknown";
const inputs = metadata.decorator?.inputs?.length || 0;
const outputs = metadata.decorator?.outputs?.length || 0;
const lifecycle = this.extractLifecycleMethods(content);
return `Angular component '${className}' (selector: ${selector})${lifecycle ? ` with ${lifecycle}` : ""
}${inputs ? `, ${inputs} inputs` : ""}${outputs ? `, ${outputs} outputs` : ""
}.`;
case "service":
const providedIn = metadata.decorator?.providedIn || "unknown";
const methods = this.extractPublicMethods(content);
return `Angular service '${className}' (providedIn: ${providedIn})${methods ? ` providing ${methods}` : ""
}.`;
case "guard":
const guardType = this.detectGuardType(content);
return `Angular ${guardType} guard '${className}' protecting routes.`;
case "directive":
const directiveSelector = metadata.decorator?.selector || "unknown";
return `Angular directive '${className}' (selector: ${directiveSelector}).`;
case "pipe":
const pipeName = metadata.decorator?.name || "unknown";
return `Angular pipe '${className}' (name: ${pipeName}) for data transformation.`;
case "module":
const imports = metadata.decorator?.imports?.length || 0;
const declarations = metadata.decorator?.declarations?.length || 0;
return `Angular module '${className}' with ${declarations} declarations and ${imports} imports.`;
case "interceptor":
return `Angular HTTP interceptor '${className}' modifying HTTP requests/responses.`;
case "resolver":
return `Angular resolver '${className}' pre-fetching route data.`;
case "validator":
return `Angular validator '${className}' for form validation.`;
default:
// Try to provide a meaningful fallback
if (className && className !== fileName) {
// Check for common patterns
if (
content.includes("signal(") ||
content.includes("computed(") ||
content.includes("effect(")
) {
return `Angular code '${className}' using signals.`;
}
if (content.includes("inject(")) {
return `Angular code '${className}' using dependency injection.`;
}
if (content.includes("Observable") || content.includes("Subject")) {
return `Angular code '${className}' with reactive streams.`;
}
return `Angular code '${className}' in ${fileName}.`;
}
// Extract first meaningful export or declaration
const exportMatch = content.match(
/export\s+(?:const|function|class|interface|type|enum)\s+(\w+)/
);
if (exportMatch) {
return `Exports '${exportMatch[1]}' from ${fileName}.`;
}
return `Angular code in ${fileName}.`;
}
}
private extractLifecycleMethods(content: string): string {
const lifecycles = [
"ngOnInit",
"ngOnChanges",
"ngOnDestroy",
"ngAfterViewInit",
"ngAfterContentInit",
];
const found = lifecycles.filter((method) => content.includes(method));
return found.length > 0 ? found.join(", ") : "";
}
private extractPublicMethods(content: string): string {
const methodMatches = content.match(/public\s+(\w+)\s*\(/g);
if (!methodMatches || methodMatches.length === 0) return "";
const methods = methodMatches
.slice(0, 3)
.map((m) => m.match(/public\s+(\w+)/)?.[1])
.filter(Boolean);
return methods.length > 0 ? `methods: ${methods.join(", ")}` : "";
}
private detectGuardType(content: string): string {
if (content.includes("CanActivate")) return "CanActivate";
if (content.includes("CanDeactivate")) return "CanDeactivate";
if (content.includes("CanLoad")) return "CanLoad";
if (content.includes("CanMatch")) return "CanMatch";
return "route";
}
private extractFirstComment(content: string): string {
const commentMatch = content.match(/\/\*\*\s*\n?\s*\*\s*(.+?)(?:\n|\*\/)/);
return commentMatch ? commentMatch[1].trim() : "";
}
private extractFirstLine(content: string): string {
const firstLine = content
.split("\n")
.find((line) => line.trim() && !line.trim().startsWith("import"));
return firstLine ? firstLine.trim().slice(0, 60) + "..." : "";
}
}