Custom Parsers
Custom parsers enable ESLint to understand non-standard JavaScript syntax, experimental features, or entirely different languages. A parser transforms source code into an Abstract Syntax Tree (AST) that ESLint can analyze.
When to Create a Parser
Create a custom parser when you need to:
Support TypeScript, Flow, or other type systems
Enable experimental JavaScript features
Lint domain-specific languages (DSLs)
Handle custom syntax extensions
Process template languages
Popular Custom Parsers:
@typescript-eslint/parser - TypeScript support
@babel/eslint-parser - Babel syntax support
vue-eslint-parser - Vue.js single-file components
Parser Interface
A parser must export either a parse() or parseForESLint() method:
parse() Method
parseForESLint() Method (Recommended)
const espree = require ( "espree" );
module . exports = {
parse ( code , options ) {
// Return only the AST
return espree . parse ( code , options );
}
};
Parser Methods
Simple parsing method that returns only the AST Parameters:
code (string): Source code to parse
options (object): Parser options from configuration
Returns: AST object
parseForESLint(code, options)
Advanced parsing method that returns AST and additional data Parameters:
code (string): Source code to parse
options (object): Parser options from configuration
Returns: Object with:
ast (required): The AST object
services (optional): Parser-dependent services
scopeManager (optional): Custom scope manager
visitorKeys (optional): Custom visitor keys
Return Object Structure
When using parseForESLint(), return an object with these properties:
ast (required)
The Abstract Syntax Tree based on ESTree specification:
{
ast : {
type : "Program" ,
body : [ ... ],
tokens : [ ... ],
comments : [ ... ],
loc : { ... },
range : [ 0 , 123 ]
}
}
services (optional)
Provide custom services to rules. For example, TypeScript parser provides type checking:
{
services : {
// TypeScript-specific service
program : tsProgram ,
esTreeNodeToTSNodeMap : new WeakMap (),
tsNodeToESTreeNodeMap : new WeakMap (),
// Custom helper
getTypeAtLocation ( node ) {
// Return type information
}
}
}
Rules can access these services:
create ( context ) {
const services = context . sourceCode . parserServices ;
if ( services . getTypeAtLocation ) {
const type = services . getTypeAtLocation ( node );
}
}
scopeManager (optional)
Custom ScopeManager for non-standard scoping:
{
scopeManager : customScopeManager
}
Requirements for scopeManager (ESLint v10.0.0+):
Must automatically resolve global variable references
Must provide addGlobals(names: string[]) method
Use eslintScopeManager: true in parserOptions for feature detection
visitorKeys (optional)
Define custom AST traversal for non-standard nodes:
{
visitorKeys : {
// Standard node
FunctionDeclaration : [ "id" , "params" , "body" ],
// Custom node type
CustomNode : [ "left" , "right" , "customProp" ]
}
}
AST Specification
Your parser must generate an AST compatible with ESLint requirements:
All Nodes
Every AST node must have:
Node type (e.g., “Identifier”, “FunctionDeclaration”)
Character indices [start, end] in source code {
range : [ 0 , 10 ] // Characters 0-10
}
Line and column location information {
loc : {
start : { line : 1 , column : 0 },
end : { line : 1 , column : 10 }
}
}
The parent property will be set by ESLint during traversal. Ensure it’s writable.
Program Node
The root node must include:
Array of tokens affecting program behavior {
tokens : [
{
type: "Keyword" ,
value: "const" ,
range: [ 0 , 5 ],
loc: { start: { ... }, end: { ... } }
}
]
}
Array of comment tokens {
comments : [
{
type: "Line" ,
value: " Comment text" ,
range: [ 10 , 24 ],
loc: { start: { ... }, end: { ... } }
}
]
}
Tokens and comments must:
Be sorted by range[0]
Not have overlapping ranges
Literal Nodes
Literal nodes must include:
The original source code text {
type : "Literal" ,
value : 42 ,
raw : "42" ,
range : [ 10 , 12 ]
}
Include metadata for better debugging and caching:
module . exports = {
meta: {
name: "eslint-parser-custom" ,
version: "1.2.3"
},
parseForESLint ( code , options ) {
// ...
}
};
Should match your npm package name
Should match your npm package version
Read metadata from package.json: const pkg = require ( "./package.json" );
module . exports = {
meta: {
name: pkg . name ,
version: pkg . version
}
};
Example: Simple Custom Parser
Here’s a basic parser that adds logging:
const espree = require ( "espree" );
module . exports = {
meta: {
name: "eslint-parser-logging" ,
version: "1.0.0"
},
parseForESLint ( code , options ) {
console . log ( `Parsing ${ options . filePath } ` );
console . time ( `Parse ${ options . filePath } ` );
const ast = espree . parse ( code , {
ecmaVersion: "latest" ,
sourceType: options . sourceType || "module" ,
ecmaFeatures: options . ecmaFeatures || {},
tokens: true ,
comment: true
});
console . timeEnd ( `Parse ${ options . filePath } ` );
return {
ast ,
services: {},
scopeManager: null ,
visitorKeys: null
};
}
};
Example: Parser with Services
Provide custom services to rules:
const espree = require ( "espree" );
module . exports = {
meta: {
name: "eslint-parser-with-services" ,
version: "1.0.0"
},
parseForESLT ( code , options ) {
const ast = espree . parse ( code , {
ecmaVersion: "latest" ,
sourceType: "module" ,
tokens: true ,
comment: true
});
// Custom service for rules
const services = {
isReactComponent ( node ) {
// Check if node is a React component
return node . type === "ClassDeclaration" &&
node . superClass &&
node . superClass . name === "Component" ;
},
getImportSource ( node ) {
// Get import source
if ( node . type === "ImportDeclaration" ) {
return node . source . value ;
}
return null ;
}
};
return {
ast ,
services ,
scopeManager: null ,
visitorKeys: null
};
}
};
Use in a rule:
module . exports = {
create ( context ) {
const services = context . sourceCode . parserServices ;
return {
ClassDeclaration ( node ) {
if ( services . isReactComponent ?.( node )) {
// Handle React components
}
}
};
}
};
Creating Your Parser
Set Up Project
Create a new npm package: mkdir eslint-parser-custom
cd eslint-parser-custom
npm init -y
Implement Parser
Create index.js: const espree = require ( "espree" );
const pkg = require ( "./package.json" );
module . exports = {
meta: {
name: pkg . name ,
version: pkg . version
},
parseForESLint ( code , options ) {
const ast = espree . parse ( code , {
ecmaVersion: options . ecmaVersion || "latest" ,
sourceType: options . sourceType || "module" ,
ecmaFeatures: options . ecmaFeatures || {},
tokens: true ,
comment: true
});
return { ast };
}
};
Test Parser
Create a test file: const parser = require ( "./index" );
const code = "const x = 1;" ;
const result = parser . parseForESLint ( code , {});
console . log ( result . ast );
Packaging and Publishing
Update package.json
{
"name" : "eslint-parser-myparser" ,
"version" : "1.0.0" ,
"main" : "index.js" ,
"keywords" : [ "eslint" , "parser" , "eslintparser" ],
"peerDependencies" : {
"eslint" : ">=9.0.0"
}
}
Use in Projects
Install: npm install --save-dev eslint-parser-myparser
Configure: import myparser from "eslint-parser-myparser" ;
export default [
{
languageOptions: {
parser: myparser
}
}
] ;
Using a Custom Parser
Configure ESLint to use your parser:
eslint.config.js (Flat Config)
.eslintrc.js (Legacy)
import customParser from "eslint-parser-custom" ;
export default [
{
files: [ "**/*.js" ] ,
languageOptions: {
parser: customParser ,
parserOptions: {
ecmaVersion: 2024 ,
sourceType: "module"
}
}
}
] ;
Parser Options
Parsers receive options from the configuration:
export default [
{
languageOptions: {
parser: customParser ,
parserOptions: {
// Standard options
ecmaVersion: 2024 ,
sourceType: "module" ,
ecmaFeatures: {
jsx: true ,
globalReturn: false
},
// Custom options for your parser
customOption: true ,
anotherOption: "value"
}
}
}
] ;
Access in parser:
parseForESLint ( code , options ) {
const ecmaVersion = options . ecmaVersion ;
const customOption = options . customOption ;
// Use options...
}
Real-World Example: TypeScript Parser
Study @typescript-eslint/parser for a complete implementation:
// Simplified structure
module . exports = {
parseForESLint ( code , options ) {
// Parse TypeScript
const tsProgram = createProgram ( code , options );
const ast = convertToESTree ( tsProgram );
// Provide TypeScript services
return {
ast ,
services: {
program: tsProgram ,
getTypeAtLocation ( node ) {
// Return TypeScript type
}
},
scopeManager ,
visitorKeys
};
}
};
Testing Strategies
Unit Tests
const assert = require ( "assert" );
const parser = require ( "./index" );
describe ( "Custom Parser" , () => {
it ( "should parse simple code" , () => {
const result = parser . parseForESLint ( "const x = 1;" , {});
assert . strictEqual ( result . ast . type , "Program" );
assert . strictEqual ( result . ast . body . length , 1 );
});
it ( "should include tokens" , () => {
const result = parser . parseForESLint ( "const x = 1;" , {});
assert ( Array . isArray ( result . ast . tokens ));
assert ( result . ast . tokens . length > 0 );
});
it ( "should handle custom syntax" , () => {
const code = "// Custom syntax" ;
const result = parser . parseForESLint ( code , {
customOption: true
});
assert ( result . ast );
});
});
Integration Tests
Test with ESLint:
const { ESLint } = require ( "eslint" );
const parser = require ( "./index" );
const eslint = new ESLint ({
overrideConfig: {
languageOptions: {
parser
},
rules: {
"no-unused-vars" : "error"
}
}
});
const results = await eslint . lintText ( "const x = 1;" );
console . log ( results );
Parsers run on every file. Optimize for performance:
Cache parse results when possible
Reuse AST nodes - don’t create unnecessary objects
Minimize memory allocations in hot paths
Profile with large files to find bottlenecks
Benchmarking
const { performance } = require ( "perf_hooks" );
const code = fs . readFileSync ( "large-file.js" , "utf8" );
const iterations = 100 ;
const start = performance . now ();
for ( let i = 0 ; i < iterations ; i ++ ) {
parser . parseForESLint ( code , {});
}
const end = performance . now ();
console . log ( `Average: ${ ( end - start ) / iterations } ms` );
Common Patterns
Wrapping Existing Parser
const espree = require ( "espree" );
module . exports = {
parseForESLint ( code , options ) {
// Pre-process code
const processedCode = preprocess ( code );
// Parse with existing parser
const ast = espree . parse ( processedCode , options );
// Post-process AST
postprocessAST ( ast );
return { ast };
}
};
Adding Custom Nodes
parseForESLint ( code , options ) {
const ast = baseParser . parse ( code , options );
// Add custom node type
traverse ( ast , {
enter ( node ) {
if ( isCustomPattern ( node )) {
node . type = "CustomNode" ;
node . customProperty = extractData ( node );
}
}
});
return {
ast ,
visitorKeys: {
... defaultVisitorKeys ,
CustomNode: [ "left" , "right" , "customProperty" ]
}
};
}
Troubleshooting
Ensure the parser is installed and correctly imported: npm list eslint-parser-custom
Verify all nodes have required properties: Check that Program has tokens and comments.
If providing custom scopeManager, ensure it:
Implements required methods
Correctly tracks variable declarations
Resolves references properly
Resources
@typescript-eslint/parser Reference implementation for TypeScript
ESTree Specification Official AST specification
Espree ESLint’s default JavaScript parser
Configure a Parser User guide for parser configuration
Next Steps
Create Custom Rules Build rules that use parser services
Create a Plugin Package parser with rules and configs