From f977ce8a48d33055640f00ddba8e03458ed01d26 Mon Sep 17 00:00:00 2001 From: Lum1104 Date: Sat, 14 Mar 2026 23:04:56 +0800 Subject: [PATCH] feat(core): add plugin registry for community analyzer plugins Introduces PluginRegistry class that discovers, registers, and manages analyzer plugins. Maps file extensions to languages, provides a unified analyzeFile entrypoint, and supports plugin priority via registration order. Foundation for the community plugin system. Co-Authored-By: Claude Opus 4.6 --- .../src/__tests__/plugin-registry.test.ts | 113 ++++++++++++++++++ packages/core/src/index.ts | 1 + packages/core/src/plugins/registry.ts | 79 ++++++++++++ 3 files changed, 193 insertions(+) create mode 100644 packages/core/src/__tests__/plugin-registry.test.ts create mode 100644 packages/core/src/plugins/registry.ts diff --git a/packages/core/src/__tests__/plugin-registry.test.ts b/packages/core/src/__tests__/plugin-registry.test.ts new file mode 100644 index 0000000..f9909ef --- /dev/null +++ b/packages/core/src/__tests__/plugin-registry.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from "vitest"; +import { PluginRegistry } from "../plugins/registry.js"; +import type { AnalyzerPlugin, StructuralAnalysis, ImportResolution } from "../types.js"; + +const emptyAnalysis: StructuralAnalysis = { + functions: [], + classes: [], + imports: [], + exports: [], +}; + +function createMockPlugin(name: string, languages: string[]): AnalyzerPlugin { + return { + name, + languages, + analyzeFile: () => ({ ...emptyAnalysis }), + resolveImports: () => [], + }; +} + +describe("PluginRegistry", () => { + it("registers a plugin", () => { + const registry = new PluginRegistry(); + const plugin = createMockPlugin("test", ["typescript"]); + registry.register(plugin); + expect(registry.getPlugins()).toHaveLength(1); + }); + + it("finds plugin by language", () => { + const registry = new PluginRegistry(); + const plugin = createMockPlugin("ts-plugin", ["typescript", "javascript"]); + registry.register(plugin); + expect(registry.getPluginForLanguage("typescript")).toBe(plugin); + expect(registry.getPluginForLanguage("javascript")).toBe(plugin); + }); + + it("returns null for unsupported language", () => { + const registry = new PluginRegistry(); + registry.register(createMockPlugin("ts-plugin", ["typescript"])); + expect(registry.getPluginForLanguage("python")).toBeNull(); + }); + + it("finds plugin by file extension", () => { + const registry = new PluginRegistry(); + const plugin = createMockPlugin("ts-plugin", ["typescript"]); + registry.register(plugin); + expect(registry.getPluginForFile("src/index.ts")).toBe(plugin); + expect(registry.getPluginForFile("src/app.tsx")).toBe(plugin); + }); + + it("maps common extensions to languages", () => { + const registry = new PluginRegistry(); + const plugin = createMockPlugin("multi", ["python", "go", "rust"]); + registry.register(plugin); + expect(registry.getPluginForFile("main.py")).toBe(plugin); + expect(registry.getPluginForFile("main.go")).toBe(plugin); + expect(registry.getPluginForFile("main.rs")).toBe(plugin); + }); + + it("lists all registered plugins", () => { + const registry = new PluginRegistry(); + registry.register(createMockPlugin("a", ["typescript"])); + registry.register(createMockPlugin("b", ["python"])); + expect(registry.getPlugins()).toHaveLength(2); + }); + + it("lists supported languages", () => { + const registry = new PluginRegistry(); + registry.register(createMockPlugin("a", ["typescript", "javascript"])); + registry.register(createMockPlugin("b", ["python"])); + const langs = registry.getSupportedLanguages(); + expect(langs).toContain("typescript"); + expect(langs).toContain("python"); + }); + + it("unregisters a plugin by name", () => { + const registry = new PluginRegistry(); + registry.register(createMockPlugin("removable", ["typescript"])); + expect(registry.getPlugins()).toHaveLength(1); + registry.unregister("removable"); + expect(registry.getPlugins()).toHaveLength(0); + }); + + it("later registration takes priority for same language", () => { + const registry = new PluginRegistry(); + const first = createMockPlugin("first", ["typescript"]); + const second = createMockPlugin("second", ["typescript"]); + registry.register(first); + registry.register(second); + expect(registry.getPluginForLanguage("typescript")?.name).toBe("second"); + }); + + it("analyzeFile delegates to correct plugin", () => { + const registry = new PluginRegistry(); + const plugin = createMockPlugin("ts-plugin", ["typescript"]); + plugin.analyzeFile = () => ({ + ...emptyAnalysis, + functions: [{ name: "hello", lineRange: [1, 5], params: [] }], + }); + registry.register(plugin); + + const result = registry.analyzeFile("src/test.ts", "const x = 1;"); + expect(result).not.toBeNull(); + expect(result!.functions).toHaveLength(1); + }); + + it("analyzeFile returns null for unsupported files", () => { + const registry = new PluginRegistry(); + registry.register(createMockPlugin("ts-plugin", ["typescript"])); + const result = registry.analyzeFile("main.py", "print('hello')"); + expect(result).toBeNull(); + }); +}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6d566f8..a0dd958 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -35,3 +35,4 @@ export { detectLanguageConcepts, type LanguageLessonResult, } from "./analyzer/language-lesson.js"; +export { PluginRegistry } from "./plugins/registry.js"; diff --git a/packages/core/src/plugins/registry.ts b/packages/core/src/plugins/registry.ts new file mode 100644 index 0000000..324a802 --- /dev/null +++ b/packages/core/src/plugins/registry.ts @@ -0,0 +1,79 @@ +import type { AnalyzerPlugin, StructuralAnalysis, ImportResolution } from "../types.js"; + +const EXTENSION_TO_LANGUAGE: Record = { + ts: "typescript", + tsx: "typescript", + js: "javascript", + jsx: "javascript", + py: "python", + go: "go", + rs: "rust", + rb: "ruby", + java: "java", + kt: "kotlin", + cs: "csharp", + cpp: "cpp", + c: "c", + swift: "swift", + php: "php", +}; + +/** + * Registry for analyzer plugins. Maps languages to plugins and provides + * a unified interface for analyzing files across languages. + */ +export class PluginRegistry { + private plugins: AnalyzerPlugin[] = []; + private languageMap = new Map(); + + register(plugin: AnalyzerPlugin): void { + this.plugins.push(plugin); + for (const lang of plugin.languages) { + this.languageMap.set(lang, plugin); + } + } + + unregister(name: string): void { + const plugin = this.plugins.find((p) => p.name === name); + if (!plugin) return; + this.plugins = this.plugins.filter((p) => p.name !== name); + this.languageMap.clear(); + for (const p of this.plugins) { + for (const lang of p.languages) { + this.languageMap.set(lang, p); + } + } + } + + getPluginForLanguage(language: string): AnalyzerPlugin | null { + return this.languageMap.get(language) ?? null; + } + + getPluginForFile(filePath: string): AnalyzerPlugin | null { + const ext = filePath.split(".").pop()?.toLowerCase(); + if (!ext) return null; + const language = EXTENSION_TO_LANGUAGE[ext]; + if (!language) return null; + return this.getPluginForLanguage(language); + } + + analyzeFile(filePath: string, content: string): StructuralAnalysis | null { + const plugin = this.getPluginForFile(filePath); + if (!plugin) return null; + return plugin.analyzeFile(filePath, content); + } + + resolveImports(filePath: string, content: string): ImportResolution[] | null { + const plugin = this.getPluginForFile(filePath); + if (!plugin) return null; + return plugin.resolveImports(filePath, content); + } + + getPlugins(): AnalyzerPlugin[] { + return [...this.plugins]; + } + + getSupportedLanguages(): string[] { + return [...this.languageMap.keys()]; + } +}