Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
}
Expand Down
26 changes: 24 additions & 2 deletions src/parser/moduleMatcherService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {}
): 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)
);
Comment on lines +31 to +35
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.entries(alternativePatterns) is executed inside the nested loops, so it’s recomputed (and reallocated) for every (useDeclaration, config) pair. Precompute the entries once outside the loops, or invert to a Map<standardPattern, altPatterns[]> / Set to avoid O(UCA) scanning and repeated allocations during parsing.

Copilot uses AI. Check for mistakes.

if (matchesStandard || matchesAlternative) {
// Only add if not already in the list
if (
!matchedConfigs.find(
c => c.declarationPattern === config.declarationPattern
Expand All @@ -25,6 +46,7 @@ export class ModuleMatcherService {
}
}
}

return matchedConfigs;
}
}
10 changes: 9 additions & 1 deletion src/parser/moduleParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -53,10 +54,17 @@ export class ModuleParser implements Parser {
definitionEntries: [],
};
}

// Get alternative declaration patterns from configuration
const alternativePatterns = ConfigurationManager.getInstance().get(
"alternativeDeclarationPatterns"
);
Comment on lines +58 to +61
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading alternativeDeclarationPatterns via ConfigurationManager inside the parser couples src/parser/* to VS Code APIs indirectly (through vscode.workspace.getConfiguration), which reduces testability/reusability of the parser. Consider fetching this setting in the VS Code-facing layer (e.g. ParsedDataProvider) and passing it into moduleParser.parse(...) / identifyConfiguredModules(...) as an optional argument so the parser remains a pure logic layer.

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +61
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ModuleParser.parse() now depends on VS Code configuration (alternativeDeclarationPatterns). However, ParsedDataProvider caches parse results by document URI + version only, so changing this setting won’t invalidate the cache and the extension may keep showing stale sections/CodeLens/definitions until the document changes. Consider including the relevant config (or a hash/version of it) in the cache key, or clearing/reparsing on onDidChangeConfiguration for ashStudio.alternativeDeclarationPatterns.

Copilot uses AI. Check for mistakes.

// Identify which modules are present
const matchedModules = this.moduleMatcherService.identifyConfiguredModules(
useDeclarations,
availableConfigs
availableConfigs,
alternativePatterns
);
if (matchedModules.length === 0) {
return {
Expand Down
1 change: 1 addition & 0 deletions src/types/extensionConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { LogLevel } from "../utils/logger";
export interface AshStudioConfig {
logLevel: LogLevel;
enableCodeLens: boolean;
alternativeDeclarationPatterns: Record<string, string>;
}
8 changes: 8 additions & 0 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
Expand Down Expand Up @@ -58,6 +63,9 @@ export class ConfigurationManager {
return {
logLevel: this.get("logLevel"),
enableCodeLens: this.get("enableCodeLens"),
alternativeDeclarationPatterns: this.get(
"alternativeDeclarationPatterns"
),
};
}
}
12 changes: 11 additions & 1 deletion test/__mocks__/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {},
Expand Down
203 changes: 203 additions & 0 deletions test/unit/parser/moduleMatcherService.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
Loading