-
Notifications
You must be signed in to change notification settings - Fork 78
@W-21102094 - Add PMD XPath custom rule creation tool with AST prompt generation #381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
aruntyagiTutu
wants to merge
28
commits into
main
Choose a base branch
from
arun.tyagi/feature/pmd_apex_rule_creation_tool
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
9f4ae16
create action declaration which helps in returning ast nodes of sampl…
aruntyagiTutu a5d4836
@W-21085164 - method to extract ast nodes from ast xml (#369)
aruntyagiTutu 2f41b07
orchestartion setps for xpath creation for apex code (#370)
aruntyagiTutu 835f99e
tool declaration (#371)
aruntyagiTutu a955aec
@W-21085164 - mcp tool added in provider (#372)
aruntyagiTutu e78f946
ast nodes generation via pmd cli method (#376)
aruntyagiTutu 1b75821
ast nodes generation via pmd cli method (#377)
aruntyagiTutu 5718c3d
@W-21102083 - creation ast local cache (#378)
aruntyagiTutu 0529310
@W-21102094 - implemented create custom rule tool (#380)
aruntyagiTutu c986c77
create custom rule implementation (#383)
aruntyagiTutu 16a02e7
@W-21102094 - Refactor create rule tool implementation (#384)
aruntyagiTutu 6ffbd3e
@W-21102094 - follow clean code for create rule tool and remove chunk…
aruntyagiTutu 8830fc0
implement patterns whereever possible (#387)
aruntyagiTutu 2b08094
path updation in code analyzer yml to be relative (#388)
aruntyagiTutu bbc6c5e
add comprehensive guidline in the prompt
aruntyagiTutu 8eaf915
custom code analyzer yaml template
aruntyagiTutu 1669e64
@W-21102094 - create rule unit tests for ast and actions functions (#…
aruntyagiTutu bd56528
Merge branch 'main' into arun.tyagi/feature/pmd_apex_rule_creation_tool
aruntyagiTutu dc9b8d0
test cases for create rule tools and services code (#393)
aruntyagiTutu 29d63e6
file ops validations (#394)
aruntyagiTutu 2934af6
add clean-all scripts for missing packages
aruntyagiTutu f687802
Merge branch 'main' into arun.tyagi/feature/pmd_apex_rule_creation_tool
aruntyagiTutu b80becb
Merge branch 'main' into arun.tyagi/feature/pmd_apex_rule_creation_tool
aruntyagiTutu 1ee9b00
remove unused field
aruntyagiTutu 54ef0a5
Merge branch 'main' into arun.tyagi/feature/pmd_apex_rule_creation_tool
aruntyagiTutu 5bef88a
Merge branch 'main' into arun.tyagi/feature/pmd_apex_rule_creation_tool
iowillhoit 1fc13a7
remove empty file
aruntyagiTutu 0dc5da1
Merge branch 'main' into arun.tyagi/feature/pmd_apex_rule_creation_tool
aruntyagiTutu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| mcp-provider-code-analyzer | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
215 changes: 215 additions & 0 deletions
215
packages/mcp-provider-code-analyzer/src/actions/create-xpath-custom-rule.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| import path from "node:path"; | ||
| import fs from "node:fs/promises"; | ||
| import { escapeXml, toSafeFilenameSlug } from "../utils.js"; | ||
|
|
||
| // Creates PMD XPath ruleset XML and updates code-analyzer.yml. | ||
|
|
||
| export type CreateXpathCustomRuleInput = { | ||
| xpath: string; | ||
| ruleName?: string; | ||
| description?: string; | ||
| language?: string; | ||
| engine?: string; | ||
| priority?: number; | ||
| workingDirectory?: string; | ||
| }; | ||
|
|
||
| export type CreateXpathCustomRuleOutput = { | ||
| status: string; | ||
| ruleXml?: string; | ||
| rulesetPath?: string; | ||
| configPath?: string; | ||
| }; | ||
|
|
||
| export interface CreateXpathCustomRuleAction { | ||
| exec(input: CreateXpathCustomRuleInput): Promise<CreateXpathCustomRuleOutput>; | ||
| } | ||
|
|
||
| export class CreateXpathCustomRuleActionImpl implements CreateXpathCustomRuleAction { | ||
| public async exec(input: CreateXpathCustomRuleInput): Promise<CreateXpathCustomRuleOutput> { | ||
| const normalized = normalizeInput(input); | ||
| if ("error" in normalized) { | ||
| return { status: normalized.error }; | ||
| } | ||
|
|
||
| const ruleXml = await buildRuleXml(normalized); | ||
| const { customRulesDir, rulesetPath, configPath } = buildPaths(normalized); | ||
| const rulesetPathForConfig = toRelativeRulesetPath(normalized.workingDirectory, rulesetPath); | ||
|
|
||
| await fs.mkdir(customRulesDir, { recursive: true }); | ||
| await fs.writeFile(rulesetPath, ruleXml, "utf8"); | ||
| await upsertCodeAnalyzerConfig(configPath, rulesetPathForConfig, normalized.engine); | ||
|
|
||
| return { status: "success", ruleXml, rulesetPath, configPath }; | ||
| } | ||
| } | ||
|
|
||
| type NormalizedInput = { | ||
| xpath: string; | ||
| engine: string; | ||
| ruleName: string; | ||
| description: string; | ||
| language: string; | ||
| priority: number; | ||
| workingDirectory: string; | ||
| }; | ||
|
|
||
| const DEFAULT_RULE_NAME = "CustomXPathRule"; | ||
| const DEFAULT_DESCRIPTION = "Generated rule from XPath"; | ||
| const DEFAULT_LANGUAGE = "apex"; | ||
| const DEFAULT_PRIORITY = 3; | ||
| const CUSTOM_RULES_DIR_NAME = "custom-rules"; | ||
|
|
||
| function normalizeInput(input: CreateXpathCustomRuleInput): NormalizedInput | { error: string } { | ||
| const xpath = (input.xpath ?? "").trim(); | ||
| if (!xpath) { | ||
| return { error: "xpath is required" }; | ||
| } | ||
|
|
||
| const engine = (input.engine ?? "pmd").toLowerCase(); | ||
| if (engine !== "pmd") { | ||
| return { error: `engine '${engine}' is not supported yet` }; | ||
| } | ||
|
|
||
| const workingDirectory = input.workingDirectory?.trim(); | ||
| if (!workingDirectory) { | ||
| return { error: "workingDirectory is required" }; | ||
| } | ||
|
|
||
| return { | ||
| xpath, | ||
| engine, | ||
| ruleName: input.ruleName?.trim() || DEFAULT_RULE_NAME, | ||
| description: input.description?.trim() || DEFAULT_DESCRIPTION, | ||
| language: (input.language ?? DEFAULT_LANGUAGE).toLowerCase(), | ||
| priority: Number.isFinite(input.priority) ? (input.priority as number) : DEFAULT_PRIORITY, | ||
| workingDirectory | ||
| }; | ||
| } | ||
|
|
||
| async function buildRuleXml(input: NormalizedInput): Promise<string> { | ||
| const templatePath = new URL("../templates/pmd-ruleset.xml", import.meta.url); | ||
| const template = await fs.readFile(templatePath, "utf8"); | ||
| return applyTemplate(template, { | ||
| rulesetName: escapeXml(input.ruleName), | ||
| rulesetDescription: escapeXml(input.description), | ||
| ruleName: escapeXml(input.ruleName), | ||
| language: escapeXml(input.language), | ||
| ruleMessage: escapeXml(input.description), | ||
| ruleDescription: escapeXml(input.description), | ||
| documentationUrl: "", | ||
| priority: String(input.priority), | ||
| xpathExpression: input.xpath, | ||
| exampleCode: "" | ||
| }); | ||
| } | ||
|
|
||
| function buildPaths(input: NormalizedInput): { customRulesDir: string; rulesetPath: string; configPath: string } { | ||
| const customRulesDir = path.join(input.workingDirectory, CUSTOM_RULES_DIR_NAME); | ||
| const safeRuleName = toSafeFilenameSlug(input.ruleName); | ||
| return { | ||
| customRulesDir, | ||
| rulesetPath: path.join(customRulesDir, `${safeRuleName}-${input.engine}-rules.xml`), | ||
| configPath: path.join(input.workingDirectory, "code-analyzer.yml") | ||
| }; | ||
| } | ||
|
|
||
| function toRelativeRulesetPath(workingDirectory: string, rulesetPath: string): string { | ||
| const relativePath = path.relative(workingDirectory, rulesetPath); | ||
| if (path.isAbsolute(relativePath) || relativePath.startsWith("..")) { | ||
| throw new Error("Ruleset path must remain within the workingDirectory."); | ||
| } | ||
| return relativePath.split(path.sep).join("/"); | ||
| } | ||
|
|
||
| function applyTemplate(template: string, values: Record<string, string>): string { | ||
| return template.replace(/\{\{(\w+)\}\}/g, (_, key: string) => values[key] ?? ""); | ||
| } | ||
|
|
||
| async function upsertCodeAnalyzerConfig(configPath: string, rulesetPath: string, engine: string): Promise<void> { | ||
| const existing = await readConfigIfExists(configPath); | ||
| if (!existing) { | ||
| await writeNewCodeAnalyzerConfig(configPath, rulesetPath, engine); | ||
| return; | ||
| } | ||
| if (existing.includes(rulesetPath)) { | ||
| return; | ||
| } | ||
| const updated = addRulesetPath(existing, rulesetPath, engine); | ||
| await fs.writeFile(configPath, updated, "utf8"); | ||
| } | ||
|
|
||
| function addRulesetPath(configContent: string, rulesetPath: string, engine: string): string { | ||
| const lines = configContent.split(/\r?\n/); | ||
| const indices = findRulesetBlockIndices(lines, engine); | ||
| if (indices.customRulesetsLineIndex !== -1) { | ||
| lines.splice(indices.customRulesetsLineIndex + 1, 0, ` - "${rulesetPath}"`); | ||
| return lines.join("\n"); | ||
| } | ||
| if (indices.engineLineIndex !== -1) { | ||
| lines.splice(indices.engineLineIndex + 1, 0, " custom_rulesets:", ` - "${rulesetPath}"`); | ||
| return lines.join("\n"); | ||
| } | ||
| return appendEngineRulesetBlock(configContent, rulesetPath, engine); | ||
| } | ||
|
|
||
| async function readConfigIfExists(configPath: string): Promise<string | null> { | ||
| try { | ||
| return await fs.readFile(configPath, "utf8"); | ||
| } catch (error) { | ||
| const code = (error as NodeJS.ErrnoException).code; | ||
| if (code === "ENOENT") { | ||
| return null; | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| async function writeNewCodeAnalyzerConfig( | ||
| configPath: string, | ||
| rulesetPath: string, | ||
| engine: string | ||
| ): Promise<void> { | ||
| const templatePath = new URL("../templates/code-analyzer.yml", import.meta.url); | ||
| const template = await fs.readFile(templatePath, "utf8"); | ||
| const content = applyTemplate(template, { rulesetPath, engine }); | ||
| await fs.writeFile(configPath, content, "utf8"); | ||
| } | ||
|
|
||
| function findRulesetBlockIndices( | ||
| lines: string[], | ||
| engine: string | ||
| ): { enginesLineIndex: number; engineLineIndex: number; customRulesetsLineIndex: number } { | ||
| let enginesLineIndex = -1; | ||
| let engineLineIndex = -1; | ||
| let customRulesetsLineIndex = -1; | ||
|
|
||
| for (let i = 0; i < lines.length; i++) { | ||
| const trimmed = lines[i].trim(); | ||
| if (trimmed === "engines:") { | ||
| enginesLineIndex = i; | ||
| continue; | ||
| } | ||
| if (trimmed === `${engine}:` && enginesLineIndex !== -1) { | ||
| engineLineIndex = i; | ||
| continue; | ||
| } | ||
| if (trimmed === "custom_rulesets:" && engineLineIndex !== -1) { | ||
| customRulesetsLineIndex = i; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| return { enginesLineIndex, engineLineIndex, customRulesetsLineIndex }; | ||
| } | ||
|
|
||
| function appendEngineRulesetBlock(configContent: string, rulesetPath: string, engine: string): string { | ||
| return [ | ||
| configContent.trimEnd(), | ||
| "", | ||
| "engines:", | ||
| ` ${engine}:`, | ||
| " custom_rulesets:", | ||
| ` - "${rulesetPath}"` | ||
| ].join("\n"); | ||
| } |
32 changes: 32 additions & 0 deletions
32
packages/mcp-provider-code-analyzer/src/actions/get-ast-nodes.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { type AstNode } from "../ast/extract-ast-nodes.js"; | ||
| import { type ApexAstNodeMetadata } from "../ast/metadata/apex-ast-reference.js"; | ||
| import { PmdAstNodePipeline } from "../ast/ast-node-pipeline.js"; | ||
|
|
||
| // Action that returns AST nodes plus cached metadata. | ||
| export type GetAstNodesInput = { | ||
| code: string; | ||
| language: string; | ||
| }; | ||
|
|
||
| export type GetAstNodesOutput = { | ||
| status: string; | ||
| nodes: AstNode[]; | ||
| metadata: ApexAstNodeMetadata[]; | ||
| }; | ||
|
|
||
| export interface GetAstNodesAction { | ||
| exec(input: GetAstNodesInput): Promise<GetAstNodesOutput>; | ||
| } | ||
|
|
||
| export class GetAstNodesActionImpl implements GetAstNodesAction { | ||
| public async exec(input: GetAstNodesInput): Promise<GetAstNodesOutput> { | ||
| try { | ||
| const pipeline = new PmdAstNodePipeline(); | ||
| const { nodes, metadata } = await pipeline.run(input); | ||
| return { status: "success", nodes, metadata }; | ||
| } catch (e) { | ||
| return { status: (e as Error)?.message ?? String(e), nodes: [], metadata: [] }; | ||
| } | ||
| } | ||
| } | ||
|
|
52 changes: 52 additions & 0 deletions
52
packages/mcp-provider-code-analyzer/src/ast/ast-node-pipeline.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { extractAstNodesFromXml, type AstNode } from "./extract-ast-nodes.js"; | ||
| import { type ApexAstNodeMetadata } from "./metadata/apex-ast-reference.js"; | ||
| import { getEngineStrategy } from "../engines/engine-strategies.js"; | ||
|
|
||
| // Template Method pipeline for AST XML -> nodes -> metadata. | ||
| export type AstPipelineInput = { | ||
| code: string; | ||
| language: string; | ||
| }; | ||
|
|
||
| export type AstPipelineOutput = { | ||
| nodes: AstNode[]; | ||
| metadata: ApexAstNodeMetadata[]; | ||
| }; | ||
|
|
||
| export abstract class AstNodePipeline { | ||
| public async run(input: AstPipelineInput): Promise<AstPipelineOutput> { | ||
| const astXml = await this.generateAstXml(input); | ||
| const nodes = this.extractNodes(astXml); | ||
| const metadata = await this.enrichMetadata(input, nodes); | ||
| return { nodes, metadata }; | ||
| } | ||
|
|
||
| protected abstract generateAstXml(input: AstPipelineInput): Promise<string>; | ||
|
|
||
| protected extractNodes(astXml: string): AstNode[] { | ||
| return extractAstNodesFromXml(astXml); | ||
| } | ||
|
|
||
| protected async enrichMetadata( | ||
| _input: AstPipelineInput, | ||
| _nodes: AstNode[] | ||
| ): Promise<ApexAstNodeMetadata[]> { | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| export class PmdAstNodePipeline extends AstNodePipeline { | ||
| private readonly strategy = getEngineStrategy("pmd"); | ||
|
|
||
| protected async generateAstXml(input: AstPipelineInput): Promise<string> { | ||
| return this.strategy.astGenerator.generateAstXml(input.code, input.language); | ||
| } | ||
|
|
||
| protected async enrichMetadata( | ||
| input: AstPipelineInput, | ||
| nodes: AstNode[] | ||
| ): Promise<ApexAstNodeMetadata[]> { | ||
| const nodeNames = Array.from(new Set(nodes.map((node) => node.nodeName))); | ||
| return this.strategy.metadataProvider.getMetadata(input.language, nodeNames); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.