1+ import fs from "node:fs" ;
2+ import path from "node:path" ;
3+ import { fileURLToPath } from "node:url" ;
4+ import { getErrorMessage } from "../utils.js" ;
5+
6+ export type SupportedEngine = 'pmd' | 'eslint' | 'regex' ;
7+
8+ export type CreateCustomRuleInput = {
9+ engine : SupportedEngine ;
10+ language : string ;
11+ } ;
12+
13+ export type CreateCustomRuleOutput = {
14+ status : string ;
15+ knowledgeBase ?: KnowledgeBase ;
16+ instructionsForLlm ?: string ;
17+ nextStep ?: {
18+ action : string ;
19+ then : string ;
20+ } ;
21+ error ?: string ;
22+ } ;
23+
24+ export type KnowledgeBase = {
25+ nodeIndex : string [ ] ;
26+ nodeInfo : Record < string , {
27+ description : string ;
28+ category ?: string ;
29+ attributes : Array < { name : string ; type : string ; description : string } > ;
30+ note ?: string ;
31+ } > ;
32+ xpathFunctions : Array < { name : string ; syntax : string ; desc : string ; returnType ?: string ; example ?: string } > ;
33+ importantNotes ?: Array < { title : string ; content : string } > ;
34+ } ;
35+
36+ export interface CreateCustomRuleAction {
37+ exec ( input : CreateCustomRuleInput ) : Promise < CreateCustomRuleOutput > ;
38+ }
39+
40+ export class CreateCustomRuleActionImpl implements CreateCustomRuleAction {
41+ private readonly knowledgeBasePath : string ;
42+
43+ constructor ( knowledgeBasePath ?: string ) {
44+ if ( knowledgeBasePath ) {
45+ this . knowledgeBasePath = knowledgeBasePath ;
46+ } else {
47+ // Resources are copied to dist/resources during build, maintaining src structure
48+ const currentDir = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
49+ this . knowledgeBasePath = path . resolve ( currentDir , '..' , 'resources' , 'custom-rules' ) ;
50+ }
51+ }
52+
53+ async exec ( input : CreateCustomRuleInput ) : Promise < CreateCustomRuleOutput > {
54+ try {
55+ if ( ! this . engineSupportsCustomRules ( input . engine ) ) {
56+ return {
57+ status : "error" ,
58+ error : `Engine '${ input . engine } ' does not support custom rules or is not yet implemented.`
59+ } ;
60+ }
61+
62+ const normalizedLanguage = input . language . toLowerCase ( ) ;
63+ const supportedLanguages = [ 'apex' ] ;
64+ if ( ! supportedLanguages . includes ( normalizedLanguage ) ) {
65+ return {
66+ status : "error" ,
67+ error : `Language '${ input . language } ' support is not yet added for the Create Custom Rule MCP tool. Currently supported languages: ${ supportedLanguages . join ( ', ' ) } .`
68+ } ;
69+ }
70+ const knowledgeBase = await this . buildPMDKnowledgeBase ( normalizedLanguage ) ;
71+
72+ return {
73+ status : "ready_for_xpath_generation" ,
74+ knowledgeBase,
75+ instructionsForLlm : this . getInstructionsForLlm ( knowledgeBase ) ,
76+ nextStep : {
77+ action : "Generate XPath rule configuration using the knowledge base" ,
78+ then : "Call apply_code_analyzer_custom_rule(rule_config_json, project_root)"
79+ }
80+ } ;
81+
82+ } catch ( e : unknown ) {
83+ return {
84+ status : "error" ,
85+ error : `Failed to prepare context: ${ getErrorMessage ( e ) } `
86+ } ;
87+ }
88+ }
89+
90+ private engineSupportsCustomRules ( engine : string ) : boolean {
91+ const supportedEngines : SupportedEngine [ ] = [ 'pmd' ] ;
92+ return supportedEngines . includes ( engine as SupportedEngine ) ;
93+ }
94+
95+ private async buildPMDKnowledgeBase ( language : string ) : Promise < KnowledgeBase > {
96+ const astReferenceFile = `${ language } -ast-reference.json` ;
97+
98+ const astReference = this . loadKnowledgeBase ( 'pmd' , astReferenceFile ) ;
99+ const xpathFunctionsData = this . loadKnowledgeBase ( 'pmd' , 'xpath-functions.json' ) ;
100+
101+ const nodeIndex = astReference . nodes . map ( ( n : any ) => n . name ) ;
102+ const nodeInfo : Record < string , any > = { } ;
103+
104+ for ( const node of astReference . nodes ) {
105+ nodeInfo [ node . name ] = {
106+ name : node . name ,
107+ description : node . description || "" ,
108+ category : node . category ,
109+ attributes : node . attributes || [ ]
110+ } ;
111+ }
112+
113+ // For Apex, only universal PMD functions are available (not Java-specific ones)
114+ const xpathFunctions = [ ] ;
115+ const universalFunctions = xpathFunctionsData . pmd_extensions ?. universal ?. functions || [ ] ;
116+ for ( const func of universalFunctions ) {
117+ xpathFunctions . push ( {
118+ name : func . name ,
119+ syntax : func . syntax ,
120+ desc : func . description ,
121+ returnType : func . returnType ,
122+ example : func . example
123+ } ) ;
124+ }
125+
126+ const importantNotes = ( language === 'apex' && astReference . important_notes )
127+ ? astReference . important_notes
128+ : [ ] ;
129+
130+ return {
131+ nodeIndex,
132+ nodeInfo,
133+ xpathFunctions,
134+ importantNotes
135+ } ;
136+ }
137+
138+ private loadKnowledgeBase ( engine : SupportedEngine , fileName : string ) : any {
139+ const filePath = path . join ( this . knowledgeBasePath , engine , fileName ) ;
140+
141+ if ( ! fs . existsSync ( filePath ) ) {
142+ throw new Error ( `Knowledge base file not found: ${ filePath } ` ) ;
143+ }
144+
145+ const content = fs . readFileSync ( filePath , 'utf-8' ) ;
146+ return JSON . parse ( content ) ;
147+ }
148+
149+ private getInstructionsForLlm ( knowledgeBase : KnowledgeBase ) : string {
150+ return `
151+
152+ YOUR TASK:
153+ Generate PMD XPath rule configuration(s) based on the user prompt and knowledge base.
154+
155+ OPTIMIZED KNOWLEDGE BASE STRUCTURE:
156+ - nodeIndex: ALL available nodes (use these names)
157+ - nodeInfo: Detailed info for frequently used nodes
158+ - xpathFunctions: PMD-specific XPath extension functions (pmd:* namespace)
159+ - importantNotes: Refer to knowledgeBase.importantNotes for critical notes about common pitfalls and correct attribute usage
160+
161+ XPATH FUNCTIONS:
162+ - PMD uses standard W3C XPath 3.1 functions (you already know these: ends-with, starts-with, contains, matches, not, and, or, string-length, etc.)
163+ - PMD-specific extension functions are provided in xpathFunctions (pmd:fileName, pmd:startLine, pmd:endLine, etc.)
164+ - Use standard XPath 3.1 functions for common operations
165+ - Use PMD extension functions (pmd:*) when you need PMD-specific capabilities
166+
167+ CRITICAL REQUIREMENTS:
168+ 1. Use ONLY node names from nodeIndex (e.g., UserClass, NOT ClassNode)
169+ 2. For nodeInfo: use provided attributes
170+ 3. READ AND FOLLOW knowledgeBase.importantNotes - they contain critical information about common mistakes
171+
172+ SEVERITY LEVELS:
173+ 1 = Critical, 2 = High, 3 = Moderate, 4 = Low, 5 = Info
174+
175+ OUTPUT FORMAT (valid JSON only, no markdown):
176+ {
177+ "xpath": "//UserClass[not(ends-with(@Image, 'Service'))]",
178+ "rule_name": "EnforceClassNamingSuffix",
179+ "message": "Class name must end with 'Service'",
180+ "severity": 2,
181+ "description": "Enforces Service suffix for all Apex class names",
182+ }
183+
184+ AFTER GENERATING THE CONFIG:
185+ Call: apply_code_analyzer_custom_rule(rule_config_json, project_root)
186+ ` ;
187+ }
188+ }
0 commit comments