diff --git a/package.json b/package.json index c164b34..36302bc 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,16 @@ "type": "boolean", "default": false, "description": "Adds the children of the dsl sections to the Outline and Symbols views" + }, + "ashStudio.alternativeDeclarationPatterns": { + "type": "object", + "default": {}, + "description": "Map alternative use declaration patterns to standard Ash patterns. For example: { \"App.Resource\": \"Ash.Resource\", \"App.Domain\": \"Ash.Domain\" } allows the extension to recognize custom macros that wrap Ash modules.", + "patternProperties": { + ".*": { + "type": "string" + } + } } } } diff --git a/src/parser/moduleMatcherService.ts b/src/parser/moduleMatcherService.ts index daee39d..7413fc8 100644 --- a/src/parser/moduleMatcherService.ts +++ b/src/parser/moduleMatcherService.ts @@ -6,15 +6,36 @@ import { ModuleConfiguration } from "../types/configurationRegistry"; export class ModuleMatcherService { /** * Takes raw use declaration strings and matches them against available configs. + * + * @param useDeclarations Array of use declaration strings found in the file + * @param availableConfigs Available module configurations to match against + * @param alternativePatterns Optional mapping of alternative patterns to standard patterns + * e.g., { "App.Resource": "Ash.Resource", "App.Domain": "Ash.Domain" } */ identifyConfiguredModules( useDeclarations: string[], - availableConfigs: ModuleConfiguration[] + availableConfigs: ModuleConfiguration[], + + alternativePatterns: Record = {} ): ModuleConfiguration[] { const matchedConfigs: ModuleConfiguration[] = []; + for (const useDeclaration of useDeclarations) { for (const config of availableConfigs) { - if (useDeclaration.includes(config.declarationPattern)) { + // Check if declaration matches the standard pattern + const matchesStandard = useDeclaration.includes( + config.declarationPattern + ); + + // Check if declaration matches any configured alternative pattern + const matchesAlternative = Object.entries(alternativePatterns).some( + ([altPattern, standardPattern]) => + standardPattern === config.declarationPattern && + useDeclaration.includes(altPattern) + ); + + if (matchesStandard || matchesAlternative) { + // Only add if not already in the list if ( !matchedConfigs.find( c => c.declarationPattern === config.declarationPattern @@ -25,6 +46,7 @@ export class ModuleMatcherService { } } } + return matchedConfigs; } } diff --git a/src/parser/moduleParser.ts b/src/parser/moduleParser.ts index efdca43..58a8c07 100644 --- a/src/parser/moduleParser.ts +++ b/src/parser/moduleParser.ts @@ -15,6 +15,7 @@ import { ModuleMatcherService } from "./moduleMatcherService"; import { DefinitionEntryService } from "./definitionEntryService"; import { SectionParser } from "./sectionParser"; import { DiagramCodeLensService } from "./diagramCodeLensService"; +import { ConfigurationManager } from "../utils/config"; /** * A Parser implementation that uses ModuleConfiguration configurations @@ -53,10 +54,17 @@ export class ModuleParser implements Parser { definitionEntries: [], }; } + + // Get alternative declaration patterns from configuration + const alternativePatterns = ConfigurationManager.getInstance().get( + "alternativeDeclarationPatterns" + ); + // Identify which modules are present const matchedModules = this.moduleMatcherService.identifyConfiguredModules( useDeclarations, - availableConfigs + availableConfigs, + alternativePatterns ); if (matchedModules.length === 0) { return { diff --git a/src/types/extensionConfiguration.ts b/src/types/extensionConfiguration.ts index 5e3ab42..6000980 100644 --- a/src/types/extensionConfiguration.ts +++ b/src/types/extensionConfiguration.ts @@ -3,4 +3,5 @@ import { LogLevel } from "../utils/logger"; export interface AshStudioConfig { logLevel: LogLevel; enableCodeLens: boolean; + alternativeDeclarationPatterns: Record; } diff --git a/src/utils/config.ts b/src/utils/config.ts index 79b37d0..42bf74b 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -29,6 +29,11 @@ export class ConfigurationManager { return config.get("logLevel", LogLevel.INFO) as AshStudioConfig[T]; case "enableCodeLens": return config.get("enableCodeLens", false) as AshStudioConfig[T]; + case "alternativeDeclarationPatterns": + return config.get( + "alternativeDeclarationPatterns", + {} + ) as AshStudioConfig[T]; default: throw new Error(`Unknown configuration key: ${key}`); } @@ -58,6 +63,9 @@ export class ConfigurationManager { return { logLevel: this.get("logLevel"), enableCodeLens: this.get("enableCodeLens"), + alternativeDeclarationPatterns: this.get( + "alternativeDeclarationPatterns" + ), }; } } diff --git a/test/__mocks__/vscode.ts b/test/__mocks__/vscode.ts index fe46493..735290c 100644 --- a/test/__mocks__/vscode.ts +++ b/test/__mocks__/vscode.ts @@ -32,7 +32,17 @@ export class Location { ) {} } -export const workspace = {}; +export const workspace = { + getConfiguration: () => ({ + get: (key: string, defaultValue?: unknown) => { + // Return default values for known configuration keys + if (key === "alternativeDeclarationPatterns") { + return defaultValue ?? {}; + } + return defaultValue; + }, + }), +}; export const window = { showInformationMessage: () => {}, diff --git a/test/unit/parser/moduleMatcherService.test.ts b/test/unit/parser/moduleMatcherService.test.ts new file mode 100644 index 0000000..d41116f --- /dev/null +++ b/test/unit/parser/moduleMatcherService.test.ts @@ -0,0 +1,203 @@ +/** + * Test suite for ModuleMatcherService + */ + +import { ModuleMatcherService } from "../../../src/parser/moduleMatcherService"; +import { ModuleConfiguration } from "../../../src/types/configurationRegistry"; + +describe("ModuleMatcherService", () => { + const mockConfigs: ModuleConfiguration[] = [ + { + displayName: "Ash.Resource", + declarationPattern: "Ash.Resource", + dslSections: [], + }, + { + displayName: "Ash.Domain", + declarationPattern: "Ash.Domain", + dslSections: [], + }, + { + displayName: "AshGraphql", + declarationPattern: "AshGraphql.Resource", + dslSections: [], + }, + ]; + + describe("identifyConfiguredModules", () => { + it("should match standard Ash.Resource declaration", () => { + const service = new ModuleMatcherService(); + const useDeclarations = ["use Ash.Resource"]; + const result = service.identifyConfiguredModules( + useDeclarations, + mockConfigs + ); + + expect(result).toHaveLength(1); + expect(result[0].declarationPattern).toBe("Ash.Resource"); + }); + + it("should match standard Ash.Domain declaration", () => { + const service = new ModuleMatcherService(); + const useDeclarations = ["use Ash.Domain"]; + const result = service.identifyConfiguredModules( + useDeclarations, + mockConfigs + ); + + expect(result).toHaveLength(1); + expect(result[0].declarationPattern).toBe("Ash.Domain"); + }); + + it("should match multiple standard declarations", () => { + const service = new ModuleMatcherService(); + const useDeclarations = [ + "use Ash.Resource", + "use Ash.Domain", + "use AshGraphql.Resource", + ]; + const result = service.identifyConfiguredModules( + useDeclarations, + mockConfigs + ); + + expect(result).toHaveLength(3); + expect(result.map(r => r.declarationPattern)).toEqual([ + "Ash.Resource", + "Ash.Domain", + "AshGraphql.Resource", + ]); + }); + + it("should not match when no declarations present", () => { + const service = new ModuleMatcherService(); + const useDeclarations = ["use SomeOtherModule"]; + const result = service.identifyConfiguredModules( + useDeclarations, + mockConfigs + ); + + expect(result).toHaveLength(0); + }); + + it("should match alternative declaration pattern App.Resource to Ash.Resource", () => { + const service = new ModuleMatcherService(); + const useDeclarations = ["use App.Resource"]; + const alternativePatterns = { + "App.Resource": "Ash.Resource", + }; + const result = service.identifyConfiguredModules( + useDeclarations, + mockConfigs, + alternativePatterns + ); + + expect(result).toHaveLength(1); + expect(result[0].declarationPattern).toBe("Ash.Resource"); + expect(result[0].displayName).toBe("Ash.Resource"); + }); + + it("should match alternative declaration pattern App.Domain to Ash.Domain", () => { + const service = new ModuleMatcherService(); + const useDeclarations = ["use App.Domain"]; + const alternativePatterns = { + "App.Domain": "Ash.Domain", + }; + const result = service.identifyConfiguredModules( + useDeclarations, + mockConfigs, + alternativePatterns + ); + + expect(result).toHaveLength(1); + expect(result[0].declarationPattern).toBe("Ash.Domain"); + expect(result[0].displayName).toBe("Ash.Domain"); + }); + + it("should match multiple alternative patterns", () => { + const service = new ModuleMatcherService(); + const useDeclarations = ["use App.Resource", "use App.Domain"]; + const alternativePatterns = { + "App.Resource": "Ash.Resource", + "App.Domain": "Ash.Domain", + }; + const result = service.identifyConfiguredModules( + useDeclarations, + mockConfigs, + alternativePatterns + ); + + expect(result).toHaveLength(2); + expect(result.map(r => r.declarationPattern)).toEqual([ + "Ash.Resource", + "Ash.Domain", + ]); + }); + + it("should match both standard and alternative patterns in same file", () => { + const service = new ModuleMatcherService(); + const useDeclarations = ["use Ash.Resource", "use App.Domain"]; + const alternativePatterns = { + "App.Domain": "Ash.Domain", + }; + const result = service.identifyConfiguredModules( + useDeclarations, + mockConfigs, + alternativePatterns + ); + + expect(result).toHaveLength(2); + expect(result.map(r => r.declarationPattern)).toEqual([ + "Ash.Resource", + "Ash.Domain", + ]); + }); + + it("should not add duplicate matches when both standard and alternative patterns match", () => { + const service = new ModuleMatcherService(); + const useDeclarations = ["use Ash.Resource", "use App.Resource"]; + const alternativePatterns = { + "App.Resource": "Ash.Resource", + }; + const result = service.identifyConfiguredModules( + useDeclarations, + mockConfigs, + alternativePatterns + ); + + // Should only have one match even though both declarations matched + expect(result).toHaveLength(1); + expect(result[0].declarationPattern).toBe("Ash.Resource"); + }); + + it("should work with empty alternative patterns object", () => { + const service = new ModuleMatcherService(); + const useDeclarations = ["use Ash.Resource"]; + const result = service.identifyConfiguredModules( + useDeclarations, + mockConfigs, + {} + ); + + expect(result).toHaveLength(1); + expect(result[0].declarationPattern).toBe("Ash.Resource"); + }); + + it("should ignore alternative patterns that don't match any use declaration", () => { + const service = new ModuleMatcherService(); + const useDeclarations = ["use Ash.Resource"]; + const alternativePatterns = { + "CustomMacro.Resource": "Ash.Resource", + }; + const result = service.identifyConfiguredModules( + useDeclarations, + mockConfigs, + alternativePatterns + ); + + // Should still match the standard Ash.Resource + expect(result).toHaveLength(1); + expect(result[0].declarationPattern).toBe("Ash.Resource"); + }); + }); +});