mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -35,3 +35,4 @@ export {
|
||||
detectLanguageConcepts,
|
||||
type LanguageLessonResult,
|
||||
} from "./analyzer/language-lesson.js";
|
||||
export { PluginRegistry } from "./plugins/registry.js";
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { AnalyzerPlugin, StructuralAnalysis, ImportResolution } from "../types.js";
|
||||
|
||||
const EXTENSION_TO_LANGUAGE: Record<string, string> = {
|
||||
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<string, AnalyzerPlugin>();
|
||||
|
||||
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()];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user