diff --git a/packages/mcp-provider-code-analyzer/package.json b/packages/mcp-provider-code-analyzer/package.json index a6f1c174..0c2676a3 100644 --- a/packages/mcp-provider-code-analyzer/package.json +++ b/packages/mcp-provider-code-analyzer/package.json @@ -41,7 +41,7 @@ "package.json" ], "scripts": { - "build": "tsc --build tsconfig.build.json --verbose", + "build": "tsc --build tsconfig.build.json --verbose && node scripts/copy-resources.js", "clean": "tsc --build tsconfig.build.json --clean", "clean-all": "yarn clean && rimraf node_modules", "lint": "eslint **/*.ts", diff --git a/packages/mcp-provider-code-analyzer/scripts/copy-resources.js b/packages/mcp-provider-code-analyzer/scripts/copy-resources.js new file mode 100644 index 00000000..83b5b8dd --- /dev/null +++ b/packages/mcp-provider-code-analyzer/scripts/copy-resources.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node +/** + * Copies resources from src/resources to dist/resources during build + * Removes existing dist/resources content first to ensure exact match + */ +import { cpSync, rmSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const packageRoot = join(__dirname, '..'); +const srcResources = join(packageRoot, 'src', 'resources'); +const distResources = join(packageRoot, 'dist', 'resources'); + +try { + console.log('📦 Copying resources to dist...'); + // Remove existing dist/resources to ensure exact match with src/resources + // This ensures files deleted from src/resources are also removed from dist/resources + rmSync(distResources, { recursive: true, force: true }); + // Copy src/resources to dist/resources + cpSync(srcResources, distResources, { recursive: true, force: true }); + console.log('✅ Resources copied successfully'); +} catch (error) { + console.error('❌ Error copying resources:', error); + process.exit(1); +} diff --git a/packages/mcp-provider-code-analyzer/src/actions/create-custom-rule.ts b/packages/mcp-provider-code-analyzer/src/actions/create-custom-rule.ts new file mode 100644 index 00000000..bac220fe --- /dev/null +++ b/packages/mcp-provider-code-analyzer/src/actions/create-custom-rule.ts @@ -0,0 +1,188 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { getErrorMessage } from "../utils.js"; + +export type SupportedEngine = 'pmd' | 'eslint' | 'regex'; + +export type CreateCustomRuleInput = { + engine: SupportedEngine; + language: string; +}; + +export type CreateCustomRuleOutput = { + status: string; + knowledgeBase?: KnowledgeBase; + instructionsForLlm?: string; + nextStep?: { + action: string; + then: string; + }; + error?: string; +}; + +export type KnowledgeBase = { + nodeIndex: string[]; + nodeInfo: Record; + note?: string; + }>; + xpathFunctions: Array<{ name: string; syntax: string; desc: string; returnType?: string; example?: string }>; + importantNotes?: Array<{ title: string; content: string }>; +}; + +export interface CreateCustomRuleAction { + exec(input: CreateCustomRuleInput): Promise; +} + +export class CreateCustomRuleActionImpl implements CreateCustomRuleAction { + private readonly knowledgeBasePath: string; + + constructor(knowledgeBasePath?: string) { + if (knowledgeBasePath) { + this.knowledgeBasePath = knowledgeBasePath; + } else { + // Resources are copied to dist/resources during build, maintaining src structure + const currentDir = path.dirname(fileURLToPath(import.meta.url)); + this.knowledgeBasePath = path.resolve(currentDir, '..', 'resources', 'custom-rules'); + } + } + + async exec(input: CreateCustomRuleInput): Promise { + try { + if (!this.engineSupportsCustomRules(input.engine)) { + return { + status: "error", + error: `Engine '${input.engine}' does not support custom rules or is not yet implemented.` + }; + } + + const normalizedLanguage = input.language.toLowerCase(); + const supportedLanguages = ['apex']; + if (!supportedLanguages.includes(normalizedLanguage)) { + return { + status: "error", + error: `Language '${input.language}' support is not yet added for the Create Custom Rule MCP tool. Currently supported languages: ${supportedLanguages.join(', ')}.` + }; + } + const knowledgeBase = await this.buildPMDKnowledgeBase(normalizedLanguage); + + return { + status: "ready_for_xpath_generation", + knowledgeBase, + instructionsForLlm: this.getInstructionsForLlm(knowledgeBase), + nextStep: { + action: "Generate XPath rule configuration using the knowledge base", + then: "Call apply_code_analyzer_custom_rule(rule_config_json, project_root)" + } + }; + + } catch (e: unknown) { + return { + status: "error", + error: `Failed to prepare context: ${getErrorMessage(e)}` + }; + } + } + + private engineSupportsCustomRules(engine: string): boolean { + const supportedEngines: SupportedEngine[] = ['pmd']; + return supportedEngines.includes(engine as SupportedEngine); + } + + private async buildPMDKnowledgeBase(language: string): Promise { + const astReferenceFile = `${language}-ast-reference.json`; + + const astReference = this.loadKnowledgeBase('pmd', astReferenceFile); + const xpathFunctionsData = this.loadKnowledgeBase('pmd', 'xpath-functions.json'); + + const nodeIndex = astReference.nodes.map((n: any) => n.name); + const nodeInfo: Record = {}; + + for (const node of astReference.nodes) { + nodeInfo[node.name] = { + name: node.name, + description: node.description || "", + category: node.category, + attributes: node.attributes || [] + }; + } + + // For Apex, only universal PMD functions are available (not Java-specific ones) + const xpathFunctions = []; + const universalFunctions = xpathFunctionsData.pmd_extensions?.universal?.functions || []; + for (const func of universalFunctions) { + xpathFunctions.push({ + name: func.name, + syntax: func.syntax, + desc: func.description, + returnType: func.returnType, + example: func.example + }); + } + + const importantNotes = (language === 'apex' && astReference.important_notes) + ? astReference.important_notes + : []; + + return { + nodeIndex, + nodeInfo, + xpathFunctions, + importantNotes + }; + } + + private loadKnowledgeBase(engine: SupportedEngine, fileName: string): any { + const filePath = path.join(this.knowledgeBasePath, engine, fileName); + + if (!fs.existsSync(filePath)) { + throw new Error(`Knowledge base file not found: ${filePath}`); + } + + const content = fs.readFileSync(filePath, 'utf-8'); + return JSON.parse(content); + } + + private getInstructionsForLlm(knowledgeBase: KnowledgeBase): string { + return ` + +YOUR TASK: +Generate PMD XPath rule configuration(s) based on the user prompt and knowledge base. + +OPTIMIZED KNOWLEDGE BASE STRUCTURE: +- nodeIndex: ALL available nodes (use these names) +- nodeInfo: Detailed info for frequently used nodes +- xpathFunctions: PMD-specific XPath extension functions (pmd:* namespace) +- importantNotes: Refer to knowledgeBase.importantNotes for critical notes about common pitfalls and correct attribute usage + +XPATH FUNCTIONS: +- PMD uses standard W3C XPath 3.1 functions (you already know these: ends-with, starts-with, contains, matches, not, and, or, string-length, etc.) +- PMD-specific extension functions are provided in xpathFunctions (pmd:fileName, pmd:startLine, pmd:endLine, etc.) +- Use standard XPath 3.1 functions for common operations +- Use PMD extension functions (pmd:*) when you need PMD-specific capabilities + +CRITICAL REQUIREMENTS: +1. Use ONLY node names from nodeIndex (e.g., UserClass, NOT ClassNode) +2. For nodeInfo: use provided attributes +3. READ AND FOLLOW knowledgeBase.importantNotes - they contain critical information about common mistakes + +SEVERITY LEVELS: +1 = Critical, 2 = High, 3 = Moderate, 4 = Low, 5 = Info + +OUTPUT FORMAT (valid JSON only, no markdown): +{ + "xpath": "//UserClass[not(ends-with(@Image, 'Service'))]", + "rule_name": "EnforceClassNamingSuffix", + "message": "Class name must end with 'Service'", + "severity": 2, + "description": "Enforces Service suffix for all Apex class names", +} + +AFTER GENERATING THE CONFIG: +Call: apply_code_analyzer_custom_rule(rule_config_json, project_root) +`; + } +} \ No newline at end of file diff --git a/packages/mcp-provider-code-analyzer/src/provider.ts b/packages/mcp-provider-code-analyzer/src/provider.ts index 11432ad9..ab1d8892 100644 --- a/packages/mcp-provider-code-analyzer/src/provider.ts +++ b/packages/mcp-provider-code-analyzer/src/provider.ts @@ -2,6 +2,7 @@ import { McpProvider, McpTool, Services } from "@salesforce/mcp-provider-api"; import { CodeAnalyzerRunMcpTool } from "./tools/run_code_analyzer.js"; import { CodeAnalyzerDescribeRuleMcpTool } from "./tools/describe_code_analyzer_rule.js"; import { CodeAnalyzerListRulesMcpTool } from "./tools/list_code_analyzer_rules.js"; +import { CreateCodeAnalyzerCustomRuleMcpTool } from "./tools/create_code_analyzer_custom_rule.js"; import {CodeAnalyzerConfigFactory, CodeAnalyzerConfigFactoryImpl} from "./factories/CodeAnalyzerConfigFactory.js"; import {EnginePluginsFactory, EnginePluginsFactoryImpl} from "./factories/EnginePluginsFactory.js"; import {RunAnalyzerActionImpl} from "./actions/run-analyzer.js"; @@ -31,7 +32,8 @@ export class CodeAnalyzerMcpProvider extends McpProvider { configFactory, enginePluginsFactory, telemetryService: services.getTelemetryService() - })) + })), + new CreateCodeAnalyzerCustomRuleMcpTool() ]); } } \ No newline at end of file diff --git a/packages/mcp-provider-code-analyzer/src/resources/custom-rules/pmd/apex-ast-reference.json b/packages/mcp-provider-code-analyzer/src/resources/custom-rules/pmd/apex-ast-reference.json new file mode 100644 index 00000000..773c8d32 --- /dev/null +++ b/packages/mcp-provider-code-analyzer/src/resources/custom-rules/pmd/apex-ast-reference.json @@ -0,0 +1,2746 @@ +{ + "description": "PMD Apex AST Nodes - Complete Reference", + "source": "Extracted from PMD 7.x source code", + "extraction_date": "2025-12-03", + "total_nodes": 97, + "version": "7.x", + "note": "Includes inherited attributes from parent classes (marked with inherited_from field)", + "important_notes": [ + { + "title": "Method Node Name", + "content": "Use @Image for Method node names, NOT @Name. @Name attribute does not exist for Method nodes." + }, + { + "title": "Interface vs Inheritance", + "content": "Use @InterfaceNames (array) for interfaces, @SuperClassName for inheritance. @SuperClassName is empty string when no superclass exists. For array attributes like @InterfaceNames, use = operator to check membership: @InterfaceNames = 'Database.Batchable'." + }, + { + "title": "MethodCallExpression", + "content": "Use @FullMethodName (NOT @MethodName) for MethodCallExpression. @FullMethodName excludes parentheses (e.g., 'Test.isRunningTest' not 'Test.isRunningTest()'). Use ancestor::Method//MethodCallExpression to search within same method context." + }, + { + "title": "Test Detection", + "content": "Use ModifierNode/@isTest for test classes/methods, NOT contains(@Image, 'Test'). String matching causes false positives. Check both class and method: not(ancestor::UserClass[ModifierNode/@isTest]) and not(ancestor::Method[ModifierNode/@isTest])." + }, + { + "title": "XPath Node Selection", + "content": "XPath must return nodes, not booleans. Place predicates inside node selectors: //Node[@attr = 'value' and condition], NOT //Node[@attr = 'value'] and condition." + }, + { + "title": "String Matching Functions", + "content": "Use XPath string functions for pattern matching: ends-with(@Image, 'Exception'), starts-with(@Image, 'Test'), contains(@Image, 'Value'). These work on @Image attribute which preserves source code casing." + }, + { + "title": "Ancestor and Descendant Navigation", + "content": "Use ancestor::Node to find containing context (e.g., ancestor::Method, ancestor::UserClass). Use descendant::Node to find contained elements (e.g., descendant::TryCatchFinallyBlockStatement). descendant:: searches all descendants at any level within the node." + }, + { + "title": "Context-Aware Method Searching", + "content": "To check if a method call exists within the same method that contains another call, use ancestor::Method//MethodCallExpression. Example: //MethodCallExpression[@FullMethodName = 'Database.query' and not(ancestor::Method//MethodCallExpression[@FullMethodName = 'String.escapeSingleQuotes'])]." + }, + { + "title": "Try-Catch Block Detection", + "content": "Use descendant::TryCatchFinallyBlockStatement to check if a method contains try-catch. Example: //Method[@Image = 'execute' and not(descendant::TryCatchFinallyBlockStatement)]. Check within method scope, not class scope." + }, + { + "title": "Annotation Detection", + "content": "Use @Image for annotation names (preserves casing). Example: //Annotation[@Image = 'TestVisible']. @Image matches the annotation name as written in source code." + }, + { + "title": "Empty String Behavior", + "content": "When checking for absence, use not(@attr = 'value') for string attributes. For @SuperClassName, empty string means no superclass, so not(@SuperClassName = 'Exception') correctly identifies classes that don't extend Exception." + }, + { + "title": "Node Type Names", + "content": "Use UserClass (not ClassNode) for Apex classes, UserTrigger (not TriggerNode) for triggers. These are the correct node type names in PMD Apex AST." + }, + { + "title": "Negation Patterns", + "content": "Use not(descendant::Node) to check if a node has no descendant of that type. Use not(ancestor::Node[@attr = 'value']) to exclude nodes based on ancestor context. Combine multiple conditions with 'and' operator." + }, + { + "title": "Node Attribute Availability", + "content": "Only use attributes that exist for the specific node type. Each node type has a defined set of attributes - check the AST reference before using @attr. For example, TriggerVariableExpression does NOT have @Image attribute, only @RealLoc and @DefiningType. Using non-existent attributes causes XPath to fail silently or return no matches." + }, + { + "title": "Node Relationships and Structure", + "content": "Understand parent-child relationships in AST. Annotations are descendants of methods, not parents. Use descendant::Annotation to find annotations within methods: //Method[descendant::Annotation[@Image = 'AuraEnabled']]. Avoid incorrect structures like //Method[...]//Annotation[...]//Method[...] which implies annotations contain methods." + }, + { + "title": "XPath Logic Direction", + "content": "Ensure XPath logic matches rule intent. To find violations, use positive conditions (e.g., @ReturnType = 'void' to find void methods). To find correct code, use negative conditions (e.g., not(@ReturnType = 'void')). Double-check that the XPath finds what you want to flag, not what you want to allow." + } + ], + "nodes": [ + { + "name": "Annotation", + "description": "Represents an annotation like @AuraEnabled, @TestVisible, @InvocableMethod", + "category": "Modifiers", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Name", + "type": "string", + "description": "Returns the normalized annotation name for known, valid annotations. The normalized name is in PascalCase. If an unknown annotation is used, the raw name (as in the source code) is returned." + }, + { + "name": "@RawName", + "type": "string", + "description": "Returns the annotation name as it appears in the source code. This allows to verify the casing." + }, + { + "name": "@Image", + "type": "string", + "description": "Returns the annotation name as it appears in the source code. This allows to verify the casing." + }, + { + "name": "@isResolved", + "type": "boolean", + "description": "Returns the annotation name as it appears in the source code. This allows to verify the casing." + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "AnnotationParameter", + "description": "Represents a parameter of an annotation", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Name", + "type": "string", + "description": "The name of this node" + }, + { + "name": "@Value", + "type": "string", + "description": "The value of this node" + }, + { + "name": "@BooleanValue", + "type": "boolean", + "description": "The booleanvalue of this node" + }, + { + "name": "@Image", + "type": "string", + "description": "The image of this node" + }, + { + "name": "@Name", + "type": "boolean", + "description": "Checks whether this annotation parameter has the given name. The check is done case-insensitive.", + "parameters": [ + "@NonNull String name" + ] + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "AnonymousClass", + "description": "Represents anonymous class", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Image", + "type": "string", + "description": "The image of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ApexFile", + "description": "Represents apex file", + "category": "Other", + "extends": "AbstractApexNode.Single", + "implements": [ + "RootNode" + ], + "attributes": [ + { + "name": "@AstInfo", + "type": "string", + "description": "The astinfo of this node" + }, + { + "name": "@MainNode", + "type": "string", + "description": "The mainnode of this node" + }, + { + "name": "@GlobalIssues", + "type": "array", + "description": "The globalissues of this node" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "The definingtype of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ArrayLoadExpression", + "description": "Represents array load expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ArrayStoreExpression", + "description": "Represents array store expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "AssignmentExpression", + "description": "Represents assignment expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Op", + "type": "string", + "description": "The op of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "BinaryExpression", + "description": "Represents a binary expression (operations with two operands like ==, !=, +, -, &&, ||)", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Op", + "type": "string", + "description": "The op of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "BindExpressions", + "description": "Represents bind expressions", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "BlockStatement", + "description": "Represents block statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@CurlyBrace", + "type": "boolean", + "description": "Whether this node hasCurlyBrace" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "BooleanExpression", + "description": "Represents boolean expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Op", + "type": "string", + "description": "The op of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "BreakStatement", + "description": "Represents break statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "CastExpression", + "description": "Represents cast expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Type", + "type": "string", + "description": "Returns the target type name of the cast expression." + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "CatchBlockStatement", + "description": "Represents catch block statement", + "category": "Statements", + "extends": "AbstractApexCommentContainerNode", + "implements": [], + "attributes": [ + { + "name": "@ExceptionType", + "type": "string", + "description": "The exceptiontype of this node" + }, + { + "name": "@VariableName", + "type": "string", + "description": "The variablename of this node" + }, + { + "name": "@Body", + "type": "string", + "description": "The body of this node" + }, + { + "name": "@ContainsComment", + "type": "boolean", + "description": "Returns true if this node contains a comment", + "inherited_from": "AbstractApexCommentContainerNode" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ClassRefExpression", + "description": "Represents class ref expression", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ConstructorPreamble", + "description": "Represents constructor preamble", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ConstructorPreambleStatement", + "description": "Represents constructor preamble statement", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ContinueStatement", + "description": "Represents continue statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "DmlDeleteStatement", + "description": "Represents a DML delete operation", + "category": "Statements", + "extends": "AbstractDmlStatement", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "DmlInsertStatement", + "description": "Represents a DML insert operation", + "category": "Statements", + "extends": "AbstractDmlStatement", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "DmlMergeStatement", + "description": "Represents dml merge statement", + "category": "Statements", + "extends": "AbstractDmlStatement", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "DmlUndeleteStatement", + "description": "Represents dml undelete statement", + "category": "Statements", + "extends": "AbstractDmlStatement", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "DmlUpdateStatement", + "description": "Represents a DML update operation", + "category": "Statements", + "extends": "AbstractDmlStatement", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "DmlUpsertStatement", + "description": "Represents dml upsert statement", + "category": "Statements", + "extends": "AbstractDmlStatement", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "DoLoopStatement", + "description": "Represents do loop statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ElseWhenBlock", + "description": "Represents else when block", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "EmptyReferenceExpression", + "description": "Represents empty reference expression", + "category": "Expressions", + "extends": "AbstractApexNode.Empty", + "implements": [], + "attributes": [ + { + "name": "@DefiningType", + "type": "string", + "description": "The definingtype of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Empty" + } + ] + }, + { + "name": "Expression", + "description": "Represents expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ExpressionStatement", + "description": "Represents expression statement", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "Field", + "description": "Represents field", + "category": "Declarations", + "extends": "AbstractApexNode.Many", + "implements": [], + "attributes": [ + { + "name": "@Image", + "type": "string", + "description": "The image of this node" + }, + { + "name": "@Type", + "type": "string", + "description": "Returns the type name.

This includes any type arguments. (This is tested.) If the type is a primitive, its case will be normalized." + }, + { + "name": "@Modifiers", + "type": "node", + "description": "Returns the type name.

This includes any type arguments. (This is tested.) If the type is a primitive, its case will be normalized." + }, + { + "name": "@Name", + "type": "string", + "description": "Returns the type name.

This includes any type arguments. (This is tested.) If the type is a primitive, its case will be normalized." + }, + { + "name": "@Value", + "type": "string", + "description": "Returns the type name.

This includes any type arguments. (This is tested.) If the type is a primitive, its case will be normalized." + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns the type name.

This includes any type arguments. (This is tested.) If the type is a primitive, its case will be normalized." + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Many" + } + ] + }, + { + "name": "FieldDeclaration", + "description": "Represents field declaration", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Image", + "type": "string", + "description": "The image of this node" + }, + { + "name": "@Name", + "type": "string", + "description": "The name of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "FieldDeclarationStatements", + "description": "Represents field declaration statements", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Modifiers", + "type": "node", + "description": "The modifiers of this node" + }, + { + "name": "@TypeName", + "type": "string", + "description": "Returns the type name.

This includes any type arguments. If the type is a primitive, its case will be normalized." + }, + { + "name": "@TypeArguments", + "type": "array", + "description": "This returns the first level of the type arguments. If there are nested types (e.g. {@code List>}), then these returned types contain themselves type arguments.

Note: This method only exists for this AST type and in no other type, even though type arguments are possible e.g. for {@link ASTVariableDeclaration#getType()}." + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ForEachStatement", + "description": "Represents for each statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ForLoopStatement", + "description": "Represents a for loop statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "FormalComment", + "description": "Represents formal comment", + "category": "Statements", + "extends": "AbstractApexNode.Empty", + "implements": [], + "attributes": [ + { + "name": "@Image", + "type": "string", + "description": "The image of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Empty" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Empty" + } + ] + }, + { + "name": "IdentifierCase", + "description": "Represents identifier case", + "category": "Other", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "IfBlockStatement", + "description": "Represents an if statement block", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "IfElseBlockStatement", + "description": "Represents if else block statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@ElseStatement", + "type": "boolean", + "description": "Whether this node hasElseStatement" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "IllegalStoreExpression", + "description": "Represents illegal store expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "InstanceOfExpression", + "description": "Represents instance of expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "InvalidDependentCompilation", + "description": "Represents invalid dependent compilation", + "category": "Other", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Image", + "type": "string", + "description": "The image of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "JavaMethodCallExpression", + "description": "Represents java method call expression", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "JavaVariableExpression", + "description": "Represents java variable expression", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "LiteralCase", + "description": "Represents literal case", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "LiteralExpression", + "description": "Represents a literal value (string, number, boolean, null)", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@LiteralType", + "type": "string", + "description": "The literaltype of this node" + }, + { + "name": "@isString", + "type": "boolean", + "description": "Whether this node isString" + }, + { + "name": "@isBoolean", + "type": "boolean", + "description": "Whether this node isBoolean" + }, + { + "name": "@isInteger", + "type": "boolean", + "description": "Whether this node isInteger" + }, + { + "name": "@isDouble", + "type": "boolean", + "description": "Whether this node isDouble" + }, + { + "name": "@isLong", + "type": "boolean", + "description": "Whether this node isLong" + }, + { + "name": "@isDecimal", + "type": "boolean", + "description": "Whether this node isDecimal" + }, + { + "name": "@isNull", + "type": "boolean", + "description": "Whether this node isNull" + }, + { + "name": "@Image", + "type": "string", + "description": "The image of this node" + }, + { + "name": "@Name", + "type": "string", + "description": "Returns the name of this literal when it is labeled in an object initializer with named arguments ({@link ASTNewKeyValueObjectExpression}).

For example, in the Apex code

{@code new X(a = 1, b = 2) }
, the {@link ASTLiteralExpression} corresponding to {@code 2} will have the {@code name} \"{@code b}\"." + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "MapEntryNode", + "description": "Represents map entry node", + "category": "Other", + "extends": "AbstractApexNode.Many", + "implements": [], + "attributes": [ + { + "name": "@Image", + "type": "string", + "description": "The image of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Many" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Many" + } + ] + }, + { + "name": "Method", + "description": "Represents a method declaration", + "category": "Declarations", + "extends": "AbstractApexNode", + "implements": [ + "ApexQualifiableNode" + ], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Internal name used by the synthetic trigger method." + }, + { + "name": "@Image", + "type": "string", + "description": "Internal name used by the synthetic trigger method." + }, + { + "name": "@CanonicalName", + "type": "string", + "description": "Internal name used by the synthetic trigger method." + }, + { + "name": "@QualifiedName", + "type": "string", + "description": "Internal name used by the synthetic trigger method." + }, + { + "name": "@isConstructor", + "type": "boolean", + "description": "Internal name used by the synthetic trigger method." + }, + { + "name": "@isStaticInitializer", + "type": "boolean", + "description": "Internal name used by the synthetic trigger method." + }, + { + "name": "@Modifiers", + "type": "node", + "description": "Internal name used by the synthetic trigger method." + }, + { + "name": "@ReturnType", + "type": "string", + "description": "Returns the method return type name. This includes any type arguments. If the type is a primitive, its case will be normalized." + }, + { + "name": "@Arity", + "type": "integer", + "description": "Returns the method return type name. This includes any type arguments. If the type is a primitive, its case will be normalized." + }, + { + "name": "@isTriggerBlock", + "type": "boolean", + "description": "Checks whether this method is the synthetic trigger method." + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode" + } + ] + }, + { + "name": "MethodBlockStatement", + "description": "Represents method block statement", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "MethodCallExpression", + "description": "Represents a method call expression", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@MethodName", + "type": "string", + "description": "The {@link Identifier}s that constitute the {@link CallExpression#getReceiver() receiver} of this method call." + }, + { + "name": "@FullMethodName", + "type": "string", + "description": "The {@link Identifier}s that constitute the {@link CallExpression#getReceiver() receiver} of this method call." + }, + { + "name": "@InputParametersSize", + "type": "integer", + "description": "The {@link Identifier}s that constitute the {@link CallExpression#getReceiver() receiver} of this method call." + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "Modifier", + "description": "Represents modifier", + "category": "Modifiers", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ModifierNode", + "description": "Represents modifier node", + "category": "Modifiers", + "extends": "AbstractApexNode.Many", + "implements": [ + "AccessNode" + ], + "attributes": [ + { + "name": "@Modifiers", + "type": "integer", + "description": "The modifiers of this node" + }, + { + "name": "@isPublic", + "type": "boolean", + "description": "Whether this node isPublic" + }, + { + "name": "@isProtected", + "type": "boolean", + "description": "Whether this node isProtected" + }, + { + "name": "@isPrivate", + "type": "boolean", + "description": "Whether this node isPrivate" + }, + { + "name": "@isAbstract", + "type": "boolean", + "description": "Whether this node isAbstract" + }, + { + "name": "@isStatic", + "type": "boolean", + "description": "Whether this node isStatic" + }, + { + "name": "@isFinal", + "type": "boolean", + "description": "Whether this node isFinal" + }, + { + "name": "@isTransient", + "type": "boolean", + "description": "Whether this node isTransient" + }, + { + "name": "@isTest", + "type": "boolean", + "description": "Returns true if function has `@isTest` annotation or `testmethod` modifier" + }, + { + "name": "@DeprecatedTestMethod", + "type": "boolean", + "description": "Returns true if function has `testmethod` modifier" + }, + { + "name": "@isTestOrTestSetup", + "type": "boolean", + "description": "Returns true if function has `testmethod` modifier" + }, + { + "name": "@isWithSharing", + "type": "boolean", + "description": "Returns true if function has `testmethod` modifier" + }, + { + "name": "@isWithoutSharing", + "type": "boolean", + "description": "Returns true if function has `testmethod` modifier" + }, + { + "name": "@isInheritedSharing", + "type": "boolean", + "description": "Returns true if function has `testmethod` modifier" + }, + { + "name": "@isWebService", + "type": "boolean", + "description": "Returns true if function has `testmethod` modifier" + }, + { + "name": "@isGlobal", + "type": "boolean", + "description": "Returns true if function has `testmethod` modifier" + }, + { + "name": "@isOverride", + "type": "boolean", + "description": "Returns true if function has `testmethod` modifier" + }, + { + "name": "@isVirtual", + "type": "boolean", + "description": "Returns true if function has `testmethod` modifier" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Many" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Many" + } + ] + }, + { + "name": "ModifierOrAnnotation", + "description": "Represents modifier or annotation", + "category": "Modifiers", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "MultiStatement", + "description": "Represents multi statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "NestedExpression", + "description": "Represents nested expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "NestedStoreExpression", + "description": "Represents nested store expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "NewKeyValueObjectExpression", + "description": "Represents new key value object expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Type", + "type": "string", + "description": "Returns the type name. This includes any type arguments. If the type is a primitive, its case will be normalized." + }, + { + "name": "@ParameterCount", + "type": "integer", + "description": "Returns the type name. This includes any type arguments. If the type is a primitive, its case will be normalized." + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "NewListInitExpression", + "description": "Represents new list init expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "NewListLiteralExpression", + "description": "Represents new list literal expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "NewMapInitExpression", + "description": "Represents new map init expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "NewMapLiteralExpression", + "description": "Represents new map literal expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "NewObjectExpression", + "description": "Represents new object expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Type", + "type": "string", + "description": "Returns the type name. This includes any type arguments. If the type is a primitive, its case will be normalized." + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "NewSetInitExpression", + "description": "Represents new set init expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "NewSetLiteralExpression", + "description": "Represents new set literal expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "PackageVersionExpression", + "description": "Represents package version expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "Parameter", + "description": "Represents parameter", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Image", + "type": "string", + "description": "The image of this node" + }, + { + "name": "@Modifiers", + "type": "node", + "description": "The modifiers of this node" + }, + { + "name": "@Type", + "type": "string", + "description": "Returns the parameter's type name.

This includes any type arguments. If the type is a primitive, its case will be normalized." + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "PostfixExpression", + "description": "Represents postfix expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Op", + "type": "string", + "description": "The op of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "PrefixExpression", + "description": "Represents prefix expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Op", + "type": "string", + "description": "The op of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "Property", + "description": "Represents property", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Type", + "type": "string", + "description": "Returns the property value's type name. This includes any type arguments. If the type is a primitive, its case will be normalized." + }, + { + "name": "@Modifiers", + "type": "node", + "description": "Returns the property value's type name. This includes any type arguments. If the type is a primitive, its case will be normalized." + }, + { + "name": "@FormatAccessorName", + "type": "string", + "description": "Returns the internal accessor (getter/setter) name of an {@link ASTProperty}. The accessor name is the constant {@link #ACCESSOR_PREFIX} prepended to the name of the property.", + "parameters": [ + "ASTProperty property" + ] + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ReferenceExpression", + "description": "Represents reference expression", + "category": "Expressions", + "extends": "AbstractApexNode.Many", + "implements": [], + "attributes": [ + { + "name": "@ReferenceType", + "type": "string", + "description": "The referencetype of this node" + }, + { + "name": "@Image", + "type": "string", + "description": "The image of this node" + }, + { + "name": "@Names", + "type": "array", + "description": "The names of this node" + }, + { + "name": "@isSafeNav", + "type": "boolean", + "description": "Whether this node isSafeNav" + }, + { + "name": "@isSObjectType", + "type": "boolean", + "description": "Whether this node isSObjectType" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Whether this node hasRealLoc" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Many" + } + ] + }, + { + "name": "ReturnStatement", + "description": "Represents a return statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "RunAsBlockStatement", + "description": "Represents run as block statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "SoqlExpression", + "description": "Represents a SOQL query expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Query", + "type": "string", + "description": "Returns the raw query as it appears in the source code." + }, + { + "name": "@CanonicalQuery", + "type": "string", + "description": "Returns the query with the SOQL keywords normalized as uppercase." + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "SoslExpression", + "description": "Represents a SOSL search expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Query", + "type": "string", + "description": "Returns the raw query as it appears in the source code." + }, + { + "name": "@CanonicalQuery", + "type": "string", + "description": "Returns the query with the SOSL keywords normalized as uppercase." + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "StandardCondition", + "description": "Represents standard condition", + "category": "Other", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "Statement", + "description": "Represents statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "StatementExecuted", + "description": "Represents statement executed", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "SuperMethodCallExpression", + "description": "Represents super method call expression", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "SuperVariableExpression", + "description": "Represents super variable expression", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "SwitchStatement", + "description": "Represents switch statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "TernaryExpression", + "description": "Represents ternary expression", + "category": "Expressions", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ThisMethodCallExpression", + "description": "Represents this method call expression", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ThisVariableExpression", + "description": "Represents this variable expression", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ThrowStatement", + "description": "Represents throw statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "TriggerVariableExpression", + "description": "Represents trigger variable expression", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "TryCatchFinallyBlockStatement", + "description": "Represents try catch finally block statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@TryBlock", + "type": "string", + "description": "The tryblock of this node" + }, + { + "name": "@CatchClauses", + "type": "array", + "description": "The catchclauses of this node" + }, + { + "name": "@FinallyBlock", + "type": "string", + "description": "The finallyblock of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "TypeWhenBlock", + "description": "Represents type when block", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Type", + "type": "string", + "description": "Returns the when block's matching type name. This includes any type arguments. If the type is a primitive, its case will be normalized." + }, + { + "name": "@Name", + "type": "string", + "description": "Returns the when block's matching type name. This includes any type arguments. If the type is a primitive, its case will be normalized." + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "UserClass", + "description": "Represents an Apex class declaration (Note: Use UserClass, not ClassNode)", + "category": "Declarations", + "extends": "BaseApexClass", + "implements": [ + "ASTUserClassOrInterface" + ], + "attributes": [ + { + "name": "@SuperClassName", + "type": "string", + "description": "Returns the name of the superclass of this class, or an empty string if there is none. The type name does NOT include type arguments." + }, + { + "name": "@InterfaceNames", + "type": "array", + "description": "Returns a list of the names of the interfaces implemented by this class. The type names do NOT include type arguments. (This is tested.)" + }, + { + "name": "@Image", + "type": "string", + "description": "Returns the name of this class/interface/enum/trigger", + "inherited_from": "BaseApexClass" + }, + { + "name": "@SimpleName", + "type": "string", + "description": "Returns the simple name of this type declaration", + "inherited_from": "BaseApexClass" + }, + { + "name": "@QualifiedName", + "type": "string", + "description": "Returns the fully qualified name of this type", + "inherited_from": "BaseApexClass" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "UserClassMethods", + "description": "Represents user class methods", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "UserEnum", + "description": "Represents an Apex enum declaration", + "category": "Declarations", + "extends": "BaseApexClass", + "implements": [], + "attributes": [ + { + "name": "@QualifiedName", + "type": "string", + "description": "The qualifiedname of this node" + }, + { + "name": "@Image", + "type": "string", + "description": "Returns the name of this class/interface/enum/trigger", + "inherited_from": "BaseApexClass" + }, + { + "name": "@SimpleName", + "type": "string", + "description": "Returns the simple name of this type declaration", + "inherited_from": "BaseApexClass" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "UserExceptionMethods", + "description": "Represents user exception methods", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "UserInterface", + "description": "Represents an Apex interface declaration", + "category": "Declarations", + "extends": "BaseApexClass", + "implements": [ + "ASTUserClassOrInterface" + ], + "attributes": [ + { + "name": "@SuperInterfaceName", + "type": "string", + "description": "Returns the name of the superclass of this class, or an empty string if there is none. The type name does NOT include type arguments." + }, + { + "name": "@Image", + "type": "string", + "description": "Returns the name of this class/interface/enum/trigger", + "inherited_from": "BaseApexClass" + }, + { + "name": "@SimpleName", + "type": "string", + "description": "Returns the simple name of this type declaration", + "inherited_from": "BaseApexClass" + }, + { + "name": "@QualifiedName", + "type": "string", + "description": "Returns the fully qualified name of this type", + "inherited_from": "BaseApexClass" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "UserTrigger", + "description": "Represents an Apex trigger declaration (Note: Use UserTrigger, not TriggerNode)", + "category": "Declarations", + "extends": "BaseApexClass", + "implements": [], + "attributes": [ + { + "name": "@TargetName", + "type": "string", + "description": "The targetname of this node" + }, + { + "name": "@Usages", + "type": "array", + "description": "The usages of this node" + }, + { + "name": "@Image", + "type": "string", + "description": "Returns the name of this class/interface/enum/trigger", + "inherited_from": "BaseApexClass" + }, + { + "name": "@SimpleName", + "type": "string", + "description": "Returns the simple name of this type declaration", + "inherited_from": "BaseApexClass" + }, + { + "name": "@QualifiedName", + "type": "string", + "description": "Returns the fully qualified name of this type", + "inherited_from": "BaseApexClass" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "ValueWhenBlock", + "description": "Represents value when block", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "VariableDeclaration", + "description": "Represents a variable declaration statement", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Image", + "type": "string", + "description": "The image of this node" + }, + { + "name": "@Type", + "type": "string", + "description": "Returns the variable's type name.

This includes any type arguments. If the type is a primitive, its case will be normalized." + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "VariableDeclarationStatements", + "description": "Represents variable declaration statements", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Modifiers", + "type": "node", + "description": "The modifiers of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "VariableExpression", + "description": "Represents a reference to a variable", + "category": "Declarations", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@Image", + "type": "string", + "description": "The image of this node" + }, + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + }, + { + "name": "WhileLoopStatement", + "description": "Represents a while loop statement", + "category": "Statements", + "extends": "AbstractApexNode.Single", + "implements": [], + "attributes": [ + { + "name": "@RealLoc", + "type": "boolean", + "description": "Returns true if this node has a real source location", + "inherited_from": "AbstractApexNode.Single" + }, + { + "name": "@DefiningType", + "type": "string", + "description": "Returns the fully qualified name of the enclosing type", + "inherited_from": "AbstractApexNode.Single" + } + ] + } + ] + } \ No newline at end of file diff --git a/packages/mcp-provider-code-analyzer/src/resources/custom-rules/pmd/templates/pmd-ruleset-template.xml b/packages/mcp-provider-code-analyzer/src/resources/custom-rules/pmd/templates/pmd-ruleset-template.xml new file mode 100644 index 00000000..77026bda --- /dev/null +++ b/packages/mcp-provider-code-analyzer/src/resources/custom-rules/pmd/templates/pmd-ruleset-template.xml @@ -0,0 +1,37 @@ + + + + {{rulesetDescription}} + + + + + {{ruleDescription}} + + + {{priority}} + + + + + + + + + + + + + + + diff --git a/packages/mcp-provider-code-analyzer/src/resources/custom-rules/pmd/xpath-functions.json b/packages/mcp-provider-code-analyzer/src/resources/custom-rules/pmd/xpath-functions.json new file mode 100644 index 00000000..e3cc2774 --- /dev/null +++ b/packages/mcp-provider-code-analyzer/src/resources/custom-rules/pmd/xpath-functions.json @@ -0,0 +1,120 @@ +{ + "description": "PMD-specific XPath extension functions", + "source": "PMD docs/_data/xpath_funs.yml", + "lastUpdated": "2025-12-08", + "pmd_extensions": { + "universal": { + "namespace": "pmd", + "description": "Functions available to ALL languages", + "functions": [ + { + "name": "fileName", + "syntax": "pmd:fileName()", + "returnType": "xs:string", + "description": "Returns the current simple file name, without path but including extension", + "since": "6.38.0", + "example": "//UserClass[pmd:fileName() = 'MyService.cls']" + }, + { + "name": "startLine", + "syntax": "pmd:startLine(element)", + "returnType": "xs:int", + "description": "Returns the line where the node starts (1-based)", + "since": "6.44.0", + "example": "//Method[pmd:startLine(.) > 100]" + }, + { + "name": "endLine", + "syntax": "pmd:endLine(element)", + "returnType": "xs:int", + "description": "Returns the line where the node ends (1-based)", + "since": "6.44.0", + "example": "//Method[pmd:endLine(.) - pmd:startLine(.) > 50]" + }, + { + "name": "startColumn", + "syntax": "pmd:startColumn(element)", + "returnType": "xs:int", + "description": "Returns the column where the node starts (1-based, inclusive)", + "since": "6.44.0", + "example": "//Statement[pmd:startColumn(.) = 1]" + }, + { + "name": "endColumn", + "syntax": "pmd:endColumn(element)", + "returnType": "xs:int", + "description": "Returns the column where the node ends (1-based, exclusive)", + "since": "6.44.0", + "example": "//Expression[pmd:endColumn(.) - pmd:startColumn(.) > 80]" + } + ] + }, + "java": { + "namespace": "pmd-java", + "description": "Functions available ONLY for Java language", + "functions": [ + { + "name": "nodeIs", + "syntax": "pmd-java:nodeIs(nodeClassName)", + "returnType": "xs:boolean", + "description": "Tests if AST node is a subtype of the given class (without 'AST' prefix)", + "example": "//*[pmd-java:nodeIs('Expression')]" + }, + { + "name": "typeIs", + "syntax": "pmd-java:typeIs(javaQualifiedName)", + "returnType": "xs:boolean", + "description": "Tests if node's static Java type is a subtype of the given type", + "example": "//VariableId[pmd-java:typeIs('java.lang.List')]" + }, + { + "name": "typeIsExactly", + "syntax": "pmd-java:typeIsExactly(javaQualifiedName)", + "returnType": "xs:boolean", + "description": "Tests if node's static type is exactly the given type (not subtypes)", + "example": "//VariableId[pmd-java:typeIsExactly('java.util.ArrayList')]" + }, + { + "name": "hasAnnotation", + "syntax": "pmd-java:hasAnnotation(annotationClassName)", + "returnType": "xs:boolean", + "description": "Tests if node has an annotation with the given qualified name", + "example": "//MethodDeclaration[pmd-java:hasAnnotation('java.lang.Override')]" + }, + { + "name": "modifiers", + "syntax": "pmd-java:modifiers()", + "returnType": "xs:string*", + "description": "Returns sequence of effective modifiers (including implicit ones)", + "example": "//MethodDeclaration[pmd-java:modifiers() = 'public']" + }, + { + "name": "explicitModifiers", + "syntax": "pmd-java:explicitModifiers()", + "returnType": "xs:string*", + "description": "Returns sequence of explicitly declared modifiers only", + "example": "//MethodDeclaration[pmd-java:explicitModifiers() = 'public']" + }, + { + "name": "metric", + "syntax": "pmd-java:metric(metricKey)", + "returnType": "xs:decimal?", + "description": "Computes and returns the value of a code metric (CYCLO, NCSS, etc.)", + "example": "//MethodDeclaration[pmd-java:metric('CYCLO') > 10]" + }, + { + "name": "matchesSig", + "syntax": "pmd-java:matchesSig(signature)", + "returnType": "xs:boolean", + "description": "Matches method/constructor call signature. Use _ as wildcard for receiver/method name", + "example": "//MethodCall[pmd-java:matchesSig('_#equals(java.lang.Object)')]" + } + ] + } + }, + "notes": { + "apex": "Apex does NOT have custom XPath functions - only the universal pmd:* functions are available", + "namespace_declaration": "Custom functions require namespace declaration in XPath, e.g., xmlns:pmd='http://pmd.sourceforge.net/pmd-core'", + "xpath_version": "PMD 7 uses XPath 3.1 via Saxon - all standard XPath 3.1 functions are available" + } +} diff --git a/packages/mcp-provider-code-analyzer/src/tools/create_code_analyzer_custom_rule.ts b/packages/mcp-provider-code-analyzer/src/tools/create_code_analyzer_custom_rule.ts new file mode 100644 index 00000000..e5acefcb --- /dev/null +++ b/packages/mcp-provider-code-analyzer/src/tools/create_code_analyzer_custom_rule.ts @@ -0,0 +1,108 @@ +import { z } from "zod"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpTool, McpToolConfig, ReleaseState, Toolset } from "@salesforce/mcp-provider-api"; +import { getErrorMessage } from "../utils.js"; +import { CreateCustomRuleAction, CreateCustomRuleActionImpl, CreateCustomRuleInput, CreateCustomRuleOutput } from "../actions/create-custom-rule.js"; + +const DESCRIPTION: string = `🚨 CALL THIS TOOL when the user asks to create custom Code Analyzer rules. + +WHEN TO USE THIS TOOL: +- User asks to create Code Analyzer custom rule(s) → CALL THIS FIRST + +This tool loads the knowledge base needed to generate rule configurations. It is the REQUIRED FIRST STEP in creating custom rules. + +⚠️ CRITICAL: ENGINE/LANGUAGE SPECIFICITY +This tool is engine and language specific. If the user requests multiple rules: +- Group rules by engine+language combination (e.g., all PMD+Apex rules together) +- Call this tool ONCE per unique engine+language combination + +💡 TOKEN OPTIMIZATION: +- If you've already called this tool for the same engine+language combination in this conversation, + the tool will return a minimal response indicating the knowledge base is already available. +- Reuse the knowledge base from your previous context instead of requesting it again. +- This significantly reduces token usage for subsequent rule generation requests. + +Example: User wants 5 rules - 3 for PMD+Apex, 2 for PMD+JavaScript +→ Call this tool 2x (once for PMD+Apex, once for PMD+JavaScript)`; + +export const inputSchema = z.object({ + engine: z.enum(['pmd', 'eslint', 'regex']).describe("Required: Which engine to create the rule for."), + language: z.string().describe("Required: The target language for the custom rule. Examples: 'apex', 'javascript', 'typescript', 'html', 'xml', 'visualforce'") }); +type InputArgsShape = typeof inputSchema.shape; + +const outputSchema = z.object({ + status: z.string().describe("'ready_for_xpath_generation' if successful, 'error' otherwise"), + knowledgeBase: z.any().optional().describe("Knowledge base for generating XPath rules."), + instructionsForLlm: z.string().optional().describe("Detailed guidelines for generating XPath"), + nextStep: z.object({ + action: z.string(), + optional: z.string().optional(), + then: z.string() + }).optional().describe("Next steps in the orchestration pattern"), + error: z.string().optional().describe("Error message if something went wrong") +}); +type OutputArgsShape = typeof outputSchema.shape; + +export class CreateCodeAnalyzerCustomRuleMcpTool extends McpTool { + public static readonly NAME: string = 'create_code_analyzer_custom_rule'; + private readonly action: CreateCustomRuleAction; + + public constructor( + action: CreateCustomRuleAction = new CreateCustomRuleActionImpl() + ) { + super(); + this.action = action; + } + + public getReleaseState(): ReleaseState { + return ReleaseState.NON_GA; + } + + public getToolsets(): Toolset[] { + return [Toolset.CODE_ANALYSIS]; + } + + public getName(): string { + return CreateCodeAnalyzerCustomRuleMcpTool.NAME; + } + + public getConfig(): McpToolConfig { + return { + title: "Create Code Analyzer Custom Rule", + description: DESCRIPTION, + inputSchema: inputSchema.shape, + outputSchema: outputSchema.shape, + annotations: { + readOnlyHint: true // This tool only reads files and documentation + } + }; + } + + public async exec(input: CreateCustomRuleInput): Promise { + let output: CreateCustomRuleOutput; + try { + validateInput(input); + output = await this.action.exec(input); + } catch (e) { + output = { + status: "error", + error: getErrorMessage(e) + }; + } + return { + content: [{ type: "text", text: JSON.stringify(output, null, 2) }], + structuredContent: output + }; + } +} + +function validateInput(input: CreateCustomRuleInput): void { + if (!input.engine) { + throw new Error("Valid engine is required."); + } + + if (!input.language || input.language.trim().length === 0) { + throw new Error("language is required and cannot be empty"); + } +} + diff --git a/packages/mcp-provider-code-analyzer/test/actions/create-custom-rule.test.ts b/packages/mcp-provider-code-analyzer/test/actions/create-custom-rule.test.ts new file mode 100644 index 00000000..96d9f35b --- /dev/null +++ b/packages/mcp-provider-code-analyzer/test/actions/create-custom-rule.test.ts @@ -0,0 +1,261 @@ +import path from "node:path"; +import { fileURLToPath } from "url"; +import fs from "node:fs"; +import { + CreateCustomRuleActionImpl, + CreateCustomRuleInput, + CreateCustomRuleOutput +} from "../../src/actions/create-custom-rule.js"; +import { expect } from "vitest"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +describe('CreateCustomRuleActionImpl', () => { + describe('When valid PMD + Apex input is provided', () => { + it('should return knowledge base with nodeIndex, nodeInfo, and xpathFunctions', async () => { + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'apex' + }; + + const action: CreateCustomRuleActionImpl = new CreateCustomRuleActionImpl(); + const output: CreateCustomRuleOutput = await action.exec(input); + + expect(output.status).toEqual('ready_for_xpath_generation'); + expect(output.knowledgeBase).toBeDefined(); + expect(output.knowledgeBase?.nodeIndex).toBeDefined(); + expect(Array.isArray(output.knowledgeBase?.nodeIndex)).toBe(true); + expect(output.knowledgeBase?.nodeIndex.length).toBeGreaterThan(0); + expect(output.knowledgeBase?.nodeInfo).toBeDefined(); + expect(typeof output.knowledgeBase?.nodeInfo).toBe('object'); + expect(output.knowledgeBase?.xpathFunctions).toBeDefined(); + expect(Array.isArray(output.knowledgeBase?.xpathFunctions)).toBe(true); + expect(output.instructionsForLlm).toBeDefined(); + expect(output.instructionsForLlm).toContain('PMD XPath rule configuration'); + expect(output.nextStep).toBeDefined(); + expect(output.nextStep?.action).toContain('Generate XPath rule configuration'); + expect(output.nextStep?.then).toContain('apply_code_analyzer_custom_rule'); + }); + + it('should include common Apex AST nodes in nodeIndex', async () => { + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'apex' + }; + + const action: CreateCustomRuleActionImpl = new CreateCustomRuleActionImpl(); + const output: CreateCustomRuleOutput = await action.exec(input); + + const nodeIndex = output.knowledgeBase?.nodeIndex || []; + // Check for some common Apex nodes + expect(nodeIndex).toContain('UserClass'); + expect(nodeIndex).toContain('Method'); + expect(nodeIndex).toContain('MethodCallExpression'); + expect(nodeIndex).toContain('ModifierNode'); + }); + + it('should include node details in nodeInfo', async () => { + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'apex' + }; + + const action: CreateCustomRuleActionImpl = new CreateCustomRuleActionImpl(); + const output: CreateCustomRuleOutput = await action.exec(input); + + const nodeInfo = output.knowledgeBase?.nodeInfo || {}; + expect(nodeInfo['UserClass']).toBeDefined(); + expect(nodeInfo['UserClass'].description).toBeDefined(); + expect(nodeInfo['UserClass'].attributes).toBeDefined(); + expect(Array.isArray(nodeInfo['UserClass'].attributes)).toBe(true); + }); + + it('should include XPath functions in xpathFunctions', async () => { + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'apex' + }; + + const action: CreateCustomRuleActionImpl = new CreateCustomRuleActionImpl(); + const output: CreateCustomRuleOutput = await action.exec(input); + + const xpathFunctions = output.knowledgeBase?.xpathFunctions || []; + expect(xpathFunctions.length).toBeGreaterThan(0); + // Check that functions have required properties + if (xpathFunctions.length > 0) { + expect(xpathFunctions[0]).toHaveProperty('name'); + expect(xpathFunctions[0]).toHaveProperty('syntax'); + expect(xpathFunctions[0]).toHaveProperty('desc'); + } + }); + + it('should handle case-insensitive language input', async () => { + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'APEX' // uppercase + }; + + const action: CreateCustomRuleActionImpl = new CreateCustomRuleActionImpl(); + const output: CreateCustomRuleOutput = await action.exec(input); + + expect(output.status).toEqual('ready_for_xpath_generation'); + expect(output.knowledgeBase).toBeDefined(); + }); + }); + + describe('When unsupported engine is provided', () => { + it('should return error for eslint engine', async () => { + const input: CreateCustomRuleInput = { + engine: 'eslint', + language: 'javascript' + }; + + const action: CreateCustomRuleActionImpl = new CreateCustomRuleActionImpl(); + const output: CreateCustomRuleOutput = await action.exec(input); + + expect(output.status).toEqual('error'); + expect(output.error).toContain('does not support custom rules'); + expect(output.knowledgeBase).toBeUndefined(); + }); + + it('should return error for regex engine', async () => { + const input: CreateCustomRuleInput = { + engine: 'regex', + language: 'apex' + }; + + const action: CreateCustomRuleActionImpl = new CreateCustomRuleActionImpl(); + const output: CreateCustomRuleOutput = await action.exec(input); + + expect(output.status).toEqual('error'); + expect(output.error).toContain('does not support custom rules'); + }); + }); + + describe('When unsupported language is provided', () => { + it('should return error for unsupported language with PMD', async () => { + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'javascript' + }; + + const action: CreateCustomRuleActionImpl = new CreateCustomRuleActionImpl(); + const output: CreateCustomRuleOutput = await action.exec(input); + + expect(output.status).toEqual('error'); + expect(output.error).toContain('support is not yet added'); + expect(output.error).toContain('Currently supported languages: apex'); + }); + + it('should return error for typescript language', async () => { + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'typescript' + }; + + const action: CreateCustomRuleActionImpl = new CreateCustomRuleActionImpl(); + const output: CreateCustomRuleOutput = await action.exec(input); + + expect(output.status).toEqual('error'); + expect(output.error).toContain('support is not yet added'); + }); + }); + + describe('When knowledge base files are missing', () => { + it('should return error when AST reference file is missing', async () => { + const invalidPath = path.join(__dirname, '..', 'fixtures', 'non-existent-path'); + const action: CreateCustomRuleActionImpl = new CreateCustomRuleActionImpl(invalidPath); + + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'apex' + }; + + const output: CreateCustomRuleOutput = await action.exec(input); + + expect(output.status).toEqual('error'); + expect(output.error).toContain('Knowledge base file not found'); + expect(output.error).toContain('apex-ast-reference.json'); + }); + + it('should return error when XPath functions file is missing', async () => { + // Create a temporary directory with only AST reference + const tempDir = path.join(__dirname, '..', 'fixtures', 'temp-kb'); + const pmdDir = path.join(tempDir, 'pmd'); + + try { + fs.mkdirSync(pmdDir, { recursive: true }); + // Copy AST reference but not XPath functions + const realAstPath = path.join(__dirname, '..', '..', 'src', 'resources', 'custom-rules', 'pmd', 'apex-ast-reference.json'); + const tempAstPath = path.join(pmdDir, 'apex-ast-reference.json'); + fs.copyFileSync(realAstPath, tempAstPath); + + const action: CreateCustomRuleActionImpl = new CreateCustomRuleActionImpl(tempDir); + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'apex' + }; + + const output: CreateCustomRuleOutput = await action.exec(input); + + expect(output.status).toEqual('error'); + expect(output.error).toContain('Knowledge base file not found'); + expect(output.error).toContain('xpath-functions.json'); + } finally { + // Cleanup + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + } + }); + }); + + describe('When custom knowledge base path is provided', () => { + it('should use custom path when provided', async () => { + const customPath = path.join(__dirname, '..', '..', 'src', 'resources', 'custom-rules'); + const action: CreateCustomRuleActionImpl = new CreateCustomRuleActionImpl(customPath); + + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'apex' + }; + + const output: CreateCustomRuleOutput = await action.exec(input); + + expect(output.status).toEqual('ready_for_xpath_generation'); + expect(output.knowledgeBase).toBeDefined(); + }); + }); + + describe('When invalid JSON in knowledge base files', () => { + it('should return error when AST reference JSON is invalid', async () => { + const tempDir = path.join(__dirname, '..', 'fixtures', 'temp-kb-invalid'); + const pmdDir = path.join(tempDir, 'pmd'); + + try { + fs.mkdirSync(pmdDir, { recursive: true }); + // Create invalid JSON file + const invalidAstPath = path.join(pmdDir, 'apex-ast-reference.json'); + fs.writeFileSync(invalidAstPath, '{ invalid json }', 'utf-8'); + + const action: CreateCustomRuleActionImpl = new CreateCustomRuleActionImpl(tempDir); + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'apex' + }; + + const output: CreateCustomRuleOutput = await action.exec(input); + + expect(output.status).toEqual('error'); + expect(output.error).toContain('Failed to prepare context'); + } finally { + // Cleanup + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + } + }); + }); +}); + diff --git a/packages/mcp-provider-code-analyzer/test/provider.test.ts b/packages/mcp-provider-code-analyzer/test/provider.test.ts index 6e871449..b5d046f2 100644 --- a/packages/mcp-provider-code-analyzer/test/provider.test.ts +++ b/packages/mcp-provider-code-analyzer/test/provider.test.ts @@ -1,6 +1,7 @@ import { McpProvider, McpTool, Services } from "@salesforce/mcp-provider-api"; import { CodeAnalyzerMcpProvider } from "../src/provider.js"; import { CodeAnalyzerRunMcpTool } from "../src/tools/run_code_analyzer.js"; +import { CreateCodeAnalyzerCustomRuleMcpTool } from "../src/tools/create_code_analyzer_custom_rule.js"; import { StubServices } from "./test-doubles.js"; import { CodeAnalyzerDescribeRuleMcpTool } from "../src/tools/describe_code_analyzer_rule.js"; import { CodeAnalyzerListRulesMcpTool } from "../src/tools/list_code_analyzer_rules.js"; @@ -20,9 +21,10 @@ describe("Tests for CodeAnalyzerMcpProvider", () => { it("When provideTools is called, then the returned array contains an CodeAnalyzerRunMcpTool instance", async () => { const tools: McpTool[] = await provider.provideTools(services); - expect(tools).toHaveLength(3); + expect(tools).toHaveLength(4); expect(tools[0]).toBeInstanceOf(CodeAnalyzerRunMcpTool); expect(tools[1]).toBeInstanceOf(CodeAnalyzerDescribeRuleMcpTool); expect(tools[2]).toBeInstanceOf(CodeAnalyzerListRulesMcpTool); + expect(tools[3]).toBeInstanceOf(CreateCodeAnalyzerCustomRuleMcpTool); }); }) \ No newline at end of file diff --git a/packages/mcp-provider-code-analyzer/test/tools/create_code_analyzer_custom_rule.test.ts b/packages/mcp-provider-code-analyzer/test/tools/create_code_analyzer_custom_rule.test.ts new file mode 100644 index 00000000..05bd849e --- /dev/null +++ b/packages/mcp-provider-code-analyzer/test/tools/create_code_analyzer_custom_rule.test.ts @@ -0,0 +1,215 @@ +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpToolConfig, ReleaseState, Toolset } from "@salesforce/mcp-provider-api"; +import { CreateCodeAnalyzerCustomRuleMcpTool } from "../../src/tools/create_code_analyzer_custom_rule.js"; +import { CreateCustomRuleAction, CreateCustomRuleInput, CreateCustomRuleOutput } from "../../src/actions/create-custom-rule.js"; + +describe("Tests for CreateCodeAnalyzerCustomRuleMcpTool", () => { + let tool: CreateCodeAnalyzerCustomRuleMcpTool; + + beforeEach(() => { + tool = new CreateCodeAnalyzerCustomRuleMcpTool(); + }); + + it("When getReleaseState is called, then 'non-ga' is returned", () => { + expect(tool.getReleaseState()).toEqual(ReleaseState.NON_GA); + }); + + it("When getToolsets is called, then 'code-analysis' is returned", () => { + expect(tool.getToolsets()).toEqual([Toolset.CODE_ANALYSIS]); + }); + + it("When getName is called, then tool name is returned", () => { + expect(tool.getName()).toEqual('create_code_analyzer_custom_rule'); + }); + + it("When getConfig is called, then the correct configuration is returned", () => { + const config: McpToolConfig = tool.getConfig(); + expect(config.title).toEqual('Create Code Analyzer Custom Rule'); + expect(config.description).toContain('CALL THIS TOOL when the user asks to create custom Code Analyzer rules'); + expect(config.inputSchema).toBeTypeOf('object'); + expect(Object.keys(config.inputSchema as object)).toEqual(['engine', 'language']); + expect(config.outputSchema).toBeTypeOf('object'); + expect(Object.keys(config.outputSchema as object)).toEqual(['status', 'knowledgeBase', 'instructionsForLlm', 'nextStep', 'error']); + expect(config.annotations).toEqual({ readOnlyHint: true }); + }); + + describe('Tests for exec method', () => { + it("When exec is called with valid inputs, then action is called with expected inputs", async () => { + const spyAction: SpyCreateCustomRuleAction = new SpyCreateCustomRuleAction(); + tool = new CreateCodeAnalyzerCustomRuleMcpTool(spyAction); + + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'apex' + }; + + const result: CallToolResult = await tool.exec(input); + + expect(spyAction.execCallHistory).toHaveLength(1); + expect(spyAction.execCallHistory[0]).toEqual(input); + + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toEqual("text"); + expect(result.content[0].text).toContain("ready_for_xpath_generation"); + expect(result.structuredContent).toBeDefined(); + expect((result.structuredContent as CreateCustomRuleOutput).status).toEqual('ready_for_xpath_generation'); + }); + + it("When exec is called with valid inputs, then action receives correct inputs", async () => { + const spyAction: SpyCreateCustomRuleAction = new SpyCreateCustomRuleAction(); + tool = new CreateCodeAnalyzerCustomRuleMcpTool(spyAction); + + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'apex' + }; + + const result: CallToolResult = await tool.exec(input); + + expect(spyAction.execCallHistory).toHaveLength(1); + expect(spyAction.execCallHistory[0].engine).toEqual('pmd'); + expect(spyAction.execCallHistory[0].language).toEqual('apex'); + expect(result.structuredContent).toBeDefined(); + }); + + + it("When exec is called with empty language, then validation error is returned", async () => { + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: '' + }; + + const result: CallToolResult = await tool.exec(input); + + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toEqual("text"); + expect(result.structuredContent).toBeDefined(); + expect((result.structuredContent as CreateCustomRuleOutput).status).toEqual('error'); + expect((result.structuredContent as CreateCustomRuleOutput).error).toContain('language is required'); + }); + + it("When exec is called with missing engine, then validation error is returned", async () => { + const input = { + language: 'apex' + // engine is missing + } as any; + + const result: CallToolResult = await tool.exec(input); + + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toEqual("text"); + expect(result.structuredContent).toBeDefined(); + expect((result.structuredContent as CreateCustomRuleOutput).status).toEqual('error'); + expect((result.structuredContent as CreateCustomRuleOutput).error).toContain('Valid engine is required'); + }); + + it('When action throws error, then return error result', async () => { + const throwingAction: ThrowingCreateCustomRuleAction = new ThrowingCreateCustomRuleAction(); + tool = new CreateCodeAnalyzerCustomRuleMcpTool(throwingAction); + + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'apex' + }; + + const result: CallToolResult = await tool.exec(input); + + const expectedOutput: CreateCustomRuleOutput = { + status: "error", + error: "Error from ThrowingCreateCustomRuleAction" + }; + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toEqual("text"); + expect(result.content[0].text).toEqual(JSON.stringify(expectedOutput, null, 2)); + expect(result.structuredContent).toEqual(expectedOutput); + }); + + it('When action returns error status, then return error result', async () => { + const errorAction: ErrorCreateCustomRuleAction = new ErrorCreateCustomRuleAction(); + tool = new CreateCodeAnalyzerCustomRuleMcpTool(errorAction); + + const input: CreateCustomRuleInput = { + engine: 'eslint', + language: 'javascript' + }; + + const result: CallToolResult = await tool.exec(input); + + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toEqual("text"); + expect(result.structuredContent).toBeDefined(); + expect((result.structuredContent as CreateCustomRuleOutput).status).toEqual('error'); + expect((result.structuredContent as CreateCustomRuleOutput).error).toContain('does not support custom rules'); + }); + + it('When exec is called with different engines, then correct engine is passed to action', async () => { + const spyAction: SpyCreateCustomRuleAction = new SpyCreateCustomRuleAction(); + tool = new CreateCodeAnalyzerCustomRuleMcpTool(spyAction); + + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'apex' + }; + + await tool.exec(input); + + expect(spyAction.execCallHistory[0].engine).toEqual('pmd'); + }); + + it('When exec is called with different languages, then correct language is passed to action', async () => { + const spyAction: SpyCreateCustomRuleAction = new SpyCreateCustomRuleAction(); + tool = new CreateCodeAnalyzerCustomRuleMcpTool(spyAction); + + const input: CreateCustomRuleInput = { + engine: 'pmd', + language: 'apex' + }; + + await tool.exec(input); + + expect(spyAction.execCallHistory[0].language).toEqual('apex'); + }); + }); +}); + +class SpyCreateCustomRuleAction implements CreateCustomRuleAction { + public execCallHistory: CreateCustomRuleInput[] = []; + public exec(input: CreateCustomRuleInput): Promise { + this.execCallHistory.push(input); + return Promise.resolve({ + status: 'ready_for_xpath_generation', + knowledgeBase: { + nodeIndex: ['UserClass', 'Method', 'MethodCallExpression'], + nodeInfo: { + 'UserClass': { + description: 'Represents an Apex class', + category: 'Class', + attributes: [] + } + }, + xpathFunctions: [] + }, + instructionsForLlm: 'Generate XPath rules', + nextStep: { + action: 'Generate XPath rule configuration', + then: 'Call apply_code_analyzer_custom_rule' + } + }); + } +} + +class ThrowingCreateCustomRuleAction implements CreateCustomRuleAction { + exec(_input: CreateCustomRuleInput): Promise { + throw new Error("Error from ThrowingCreateCustomRuleAction"); + } +} + +class ErrorCreateCustomRuleAction implements CreateCustomRuleAction { + exec(input: CreateCustomRuleInput): Promise { + return Promise.resolve({ + status: 'error', + error: `Engine '${input.engine}' does not support custom rules or is not yet implemented.` + }); + } +} + diff --git a/packages/mcp-provider-dx-core/test/e2e/tool-registration.test.ts b/packages/mcp-provider-dx-core/test/e2e/tool-registration.test.ts index b498cf31..eaca6d90 100644 --- a/packages/mcp-provider-dx-core/test/e2e/tool-registration.test.ts +++ b/packages/mcp-provider-dx-core/test/e2e/tool-registration.test.ts @@ -90,7 +90,7 @@ describe('specific tool registration', () => { try { const initialTools = (await client.listTools()).tools.map((t) => t.name).sort(); - expect(initialTools.length).to.equal(6); + expect(initialTools.length).to.equal(7); expect(initialTools).to.deep.equal( [ 'run_soql_query', @@ -99,6 +99,7 @@ describe('specific tool registration', () => { 'describe_code_analyzer_rule', 'run_code_analyzer', 'list_code_analyzer_rules', + 'create_code_analyzer_custom_rule' ].sort(), ); } catch (err) {