how to write maintainable custom static analysis linting

how to write maintainable custom static analysis linting rules using typescript abstract syntax trees is the central topic of this article, and the following content explains the key points, practical uses, and decisions around it.

Writing clean, consistent code is a cornerstone of maintainable software, but off-the-shelf linters often can’t account for project-specific conventions or business rules—this is where learning how to write maintainable custom static analysis linting rules using typescript abstract syntax trees (AST) becomes invaluable. Unlike basic text-based lint checks, AST-based rules work with the structured, parsed representation of your code, enabling precise, reliable analysis that catches subtle bugs or style inconsistencies that regex-based checks miss. TypeScript, with its typed AST, offers a powerful framework for building these rules, as it preserves type information and language features specific to TypeScript, making it easier to write targeted, maintainable logic that adapts to your project’s unique needs.

Understanding TypeScript AST and ESLint’s Core Integration

To build effective custom linters, you first need to grasp how TypeScript converts code into an AST and how ESLint leverages this structure to run rules. When TypeScript transpiles code, it parses each line into a hierarchical tree of nodes, where every node corresponds to a syntactic element—such as function declarations, class methods, or import statements—each with a unique type and set of properties. This structure eliminates ambiguity, so you don’t have to parse raw text to understand code intent, a huge advantage for static analysis tools like ESLint.

How TypeScript’s AST Differs from JavaScript’s

Unlike JavaScript’s untyped AST, TypeScript’s AST includes nodes for TypeScript-specific syntax: interfaces, type aliases, decorators, and generics. This means when building lint rules for TypeScript, you can directly target these elements, avoiding the need to work around language gaps. For example, a rule enforcing consistent decorator usage can select nodes of type TSDecorator, something JavaScript-only linters can’t do natively. This type richness also lets you validate type-related patterns, like ensuring a function’s return type matches its implementation, with far more accuracy than text-based checks.

ESLint’s Rule Execution Model

ESLint acts as a bridge between your code, the TypeScript AST, and your custom rules. When you run ESLint with TypeScript support, it uses @typescript-eslint/parser to parse code into the TypeScript AST, then traverses the tree to apply each custom rule. Each rule defines two key parts: a selector (to target specific AST nodes) and a listener function (to validate the node and report errors if violations are found). This modular design makes it straightforward to build rules that focus on specific syntax patterns, rather than rewriting entire linter frameworks from scratch.

Best Practices for Maintainable Custom Linting Rules

Maintainability is critical for custom lint rules, especially as your project grows and TypeScript versions update. Poorly written rules can become technical debt, hard to debug or extend over time. Following structured practices ensures your rules stay reliable and easy to update. One core principle is to separate reusable logic from rule-specific code, so you don’t repeat the same parsing or validation steps across multiple rules.

  • Modularize common AST traversal logic into shared utility functions to reduce code duplication across rules
  • Document each rule’s purpose, valid cases, invalid cases, and configuration options to help team members understand how to use and adjust it
  • Write test cases that cover edge scenarios—like empty functions, arrow functions, and async functions—to ensure the rule behaves correctly in all contexts
  • Align rule naming with ESLint conventions (e.g., no-invalid-jsdoc for a JSDoc-related rule) to keep your rule set consistent with standard ESLint packages

Another key practice is to use TypeScript for writing your custom rules, which leverages type safety to catch errors during development. TypeScript’s type checking ensures you’re working with valid AST node properties, reducing runtime bugs and making your rules more robust. You should also avoid overcomplicating rules: smaller, focused rules are easier to maintain and debug, especially when TypeScript updates introduce changes to AST node structures.

Building a Practical Example Rule for TypeScript Projects

Let’s put these concepts into action by building a rule that enforces exported functions include JSDoc comments in TypeScript projects. This is a common convention to ensure function intent is clear for other developers working with the codebase, especially in public-facing libraries. The rule will target exported function declarations and check that they have at least one JSDoc comment describing their purpose.

Selecting Target Nodes with AST Selectors

First, we need to select the correct AST nodes: exported function declarations. ESLint’s selectors let us target nodes of type FunctionDeclaration that have an export modifier. The selector string would be something like "FunctionDeclaration[export=true]", which tells ESLint to only process function declarations that are marked as exported. This avoids checking non-exported functions, which don’t need public documentation, keeping the rule focused and efficient.

Implementing the Core Validation Logic

Next, we write the listener function for the rule. For each selected function node, we check if it has a JSDoc comment attached. In the TypeScript AST, comments are stored in the leadingComments property of the node, so we verify if any comment starts with /* or /**. If no valid JSDoc comment is found, we report an error with a message explaining that exported functions require JSDoc documentation. We also extend the selector to include arrow functions assigned to variables (like const myFunc = () => {}) to ensure all exported function patterns are covered, not just named function declarations. To test this rule, you’ll use ESLint’s RuleTester from @typescript-eslint/rule-tester, which lets you define test cases and verify the rule reports errors correctly.

Testing this rule involves creating test files with valid cases (exported functions with JSDoc) and invalid cases (exported functions without JSDoc), plus edge cases like functions with multiple leading comments or deprecated JSDoc formats. This ensures the rule works as intended across different code patterns, even as TypeScript’s AST evolves.

Learning how to write maintainable custom static analysis linting rules using typescript abstract syntax trees is a game-changer for TypeScript projects, allowing teams to enforce project-specific rules that standard linters can’t. By understanding the TypeScript AST structure, following maintainability best practices, and building rules with clear, modular logic, you can create lint rules that adapt to your project’s needs over time. The example rule we built demonstrates how concrete, targeted checks can improve code quality, while practices like testing and documentation ensure your rules remain valuable as your codebase evolves. With these foundations, you can create custom linters that catch unique issues, reduce code review back-and-forth, and keep your TypeScript codebase consistent and maintainable for years to come.

All content about “how to write maintainable custom static analysis linting rules using typescript abstract syntax trees” is originally written by our expert editorial team. Reproduction without permission is strictly prohibited.