Understand Apex language rules, ANTLR grammar structure, and how to implement parser listeners for language server features. Use when working on Apex parser implementation, creating validators, implementing listeners, or referencing parser grammar rules.
The Apex parser uses ANTLR grammar files that are not bundled in the module. Reference them from GitHub:
Grammar Files Location:
BaseApexParser.g4 - Defines parser rules (compilationUnit, classDeclaration, methodDeclaration, etc.)BaseApexLexer.g4 - Defines tokenization rulesKey Grammar Rules:
compilationUnit - Entry point for Apex class filestriggerUnit - Entry point for trigger filesanonymousUnit - Entry point for anonymous Apex blocksclassDeclaration, interfaceDeclaration, enumDeclaration - Type declarationsmethodDeclaration, constructorDeclaration - Method/constructor declarationsWhen implementing listeners or validators, reference the grammar to understand:
ClassDeclarationContext, MethodDeclarationContext)Apex has no import statements—never generate or expect them. The compiler resolves types by:
Fully qualified names (<namespace>.<TypeName>) are used only when a name conflict exists; otherwise, use the unqualified name.
Important: A namespace and a type in that namespace can share the same name (e.g., Acme.Acme) and this is valid. Namespace is determined by org/package metadata, not declared in code.
Final and Virtual:
final keyword on classes or methods (syntax error)virtual keyword to make classes/methods extensible/overridablefinal keyword can only be used for variables (prevents reassignment)Example:
// Correct: Normal class (final by default)
public class MyClass { }
// Incorrect: Cannot use 'final' keyword on classes
public final class MyClass { } // Syntax error!
// Correct: Virtual class (can be extended)
public virtual class MyClass { }
// Correct: Final variable
public final Integer count = 5;
.cls files: Use compilationUnit parser rule.trigger files: Use triggerUnit parser rule.apex files: Use anonymousUnit parser rule (wrapped in block)The Apex language grammar only allows a single top-level type within the same file. When creating test artifacts:
All custom listeners extend BaseApexParserListener<T>:
import { BaseApexParserListener } from './BaseApexParserListener';
import { ClassDeclarationContext } from '@apexdevtools/apex-parser';
export class MyListener extends BaseApexParserListener<MyResultType> {
enterClassDeclaration(ctx: ClassDeclarationContext): void {
// Handle class declaration entry
}
exitClassDeclaration(ctx: ClassDeclarationContext): void {
// Handle class declaration exit
}
// Override getResult() to return your result
getResult(): MyResultType {
return this.result;
}
}
Listener methods correspond to parser rules:
enter* methods: Called when entering a rule contextexit* methods: Called when exiting a rule contextCommon Context Types:
ClassDeclarationContext - Class declarationsMethodDeclarationContext - Method declarationsConstructorDeclarationContext - Constructor declarationsVariableDeclaratorContext - Variable declarationsExpressionContext - ExpressionsStatementContext - Statementsimport { CompilerService } from '@salesforce/apex-lsp-parser-ast';
import { MyListener } from './MyListener';
const compiler = new CompilerService();
const listener = new MyListener();
const result = compiler.compile(fileContent, fileName, listener);
// Access result
const myResult = result.result;
const errors = result.errors;
const warnings = result.warnings;
Each parser rule generates a context class with:
start/stop tokensExample:
enterMethodDeclaration(ctx: MethodDeclarationContext): void {
// Access method name
const methodName = ctx.id()?.text;
// Access return type
const returnType = ctx.typeRef()?.text;
// Access modifiers
const modifiers = ctx.modifier();
// Access location for errors
const location = {
start: ctx.start.line,
end: ctx.stop?.line,
};
}
To discover available parser rules:
BaseApexParser.g4methodDeclaration, classDeclaration){RuleName}Context@apexdevtools/apex-parserScope Tracking:
export class ScopeTrackingListener extends BaseApexParserListener<SymbolTable> {
private scopeStack: ApexSymbol[] = [];
enterClassDeclaration(ctx: ClassDeclarationContext): void {
const classSymbol = this.createClassSymbol(ctx);
const blockSymbol = this.createBlockSymbol('class', ctx);
this.scopeStack.push(blockSymbol);
}
exitClassDeclaration(): void {
this.scopeStack.pop();
}
getCurrentScope(): ApexSymbol {
return this.scopeStack[this.scopeStack.length - 1];
}
}
Error Reporting:
enterMethodDeclaration(ctx: MethodDeclarationContext): void {
if (this.errorListener) {
this.errorListener.addError({
message: 'Custom error message',
line: ctx.start.line,
column: ctx.start.charPositionInLine,
});
}
}
Fast, same-file validations that run on every keystroke (<500ms):
Comprehensive validations that may require cross-file analysis:
When working with semantics and validation, error codes and messages must be aligned with the Salesforce org compiler.
Error Messages Reference:
packages/apex-parser-ast/src/resources/messages/messages_en_US.properties
error.code.key=Error message text with {0} placeholderspackages/apex-parser-ast/src/generated/ErrorCodes.ts - Error code constantspackages/apex-parser-ast/src/generated/messages_en_US.ts - Error message mappingsWorkflow for Adding New Error Codes:
Check for existing error code: First, search messages_en_US.properties to see if an appropriate error code already exists
If a new error code is needed: Ask the user for permission before proceeding
Edit the properties file: After receiving permission, add your new error code to messages_en_US.properties
my.new.error.code=Error message text with {0} placeholder
Regenerate TypeScript files: Run npm run precompile in packages/apex-parser-ast
ErrorCodes.ts and messages_en_US.ts from the properties fileUse in validators: Import and use the generated constants
import { ErrorCodes } from '../generated/ErrorCodes';
// Use ErrorCodes.MY_NEW_ERROR_CODE in your validator
Critical: When implementing validators that require error reporting, if you determine that a new error code needs to be created, you must ask the user for permission before proceeding. Do not create new error codes without explicit user approval.
When to Consult This File:
Example Error Codes:
invalid.void.parameter - Parameters cannot be of type voidunreachable.statement - Unreachable statementinvalid.constructor.return - Constructors must not return a valueinvalid.super.call - Call to 'super()' must be the first statement in a constructor methodImportant: Always consult messages_en_US.properties when:
.properties file, then run npm run precompile to regenerate TS filesValidators can use listeners to traverse parse trees:
import { BaseApexParserListener } from '../../../parser/listeners/BaseApexParserListener';
class MyValidatorListener extends BaseApexParserListener<void> {
enterMethodDeclaration(ctx: MethodDeclarationContext): void {
// Validate method
if (ctx.parameterList()?.parameter().length > 32) {
this.addError('Too many parameters');
}
}
}
@apexdevtools/apex-parser (v4.4.1+)packages/apex-parser-ast/README.md - Comprehensive architecture documentationpackages/apex-parser-ast/src/parser/listeners/ - Reference implementations