td-antlers-linter
v0.2.2
Published
`antlers-linter` is a small Antlers template linting library built on top of [`td-antlers-parser`](https://www.npmjs.com/package/td-antlers-parser). It parses template text into an AST, walks the node tree, and runs rule listeners against each node to pro
Readme
antlers-linter
antlers-linter is a small Antlers template linting library built on top of td-antlers-parser. It parses template text into an AST, walks the node tree, and runs rule listeners against each node to produce diagnostics and fix suggestions.
The package exposes both a programmatic API and a CLI (antlers-lint) suitable for CI use.
The source is authored in TypeScript and compiled to dist/ for consumption.
Project Structure
.
├── package.json
├── tsconfig.json
├── bin
│ └── antlers-lint.mjs
├── src
│ ├── cli.ts
│ ├── fix.ts
│ ├── formatters.ts
│ ├── index.ts
│ ├── linter.ts
│ ├── types.ts
│ └── rules
│ └── *.ts
├── type-tests
│ └── public-api.ts
└── test
├── cli.test.ts
├── config.test.ts
├── custom-rules.test.ts
├── lint-text.test.ts
└── rules
└── *.test.tsWhat each file does
bin/antlers-lint.mjsis the thin shebang entry thatnpm/npxresolves to when invoking the CLI.src/cli.tsparses CLI arguments, discovers files via globs, loads config, runs the linter, applies--fix, and writes formatter output.src/fix.tsapplies non-overlapping auto-fixes from diagnostics to a source string.src/formatters.tsimplements thestylish,json, andgithub-actionsreporters.src/index.tsre-exports the public API and public TypeScript types.src/types.tsdeclares the linter, rule, config, and diagnostic contracts.src/linter.tscontains the core linter engine:- built-in rule registration
- the recommended default config
- config loading from disk
- AST traversal
- rule context creation
- disable-directive handling
- diagnostic and suggestion generation
src/rules/contains the built-in Antlers rules shipped by the package.type-tests/public-api.tsis a compile-time smoke test for the exported API and types.test/cli.test.tscovers argument parsing, file discovery, formatter output,--fix,--quiet,--max-warnings,--config, and exit codes.test/config.test.tscovers config discovery and merge behavior for.antlerslintrc.json,antlerslint.config.js, andpackage.json.test/custom-rules.test.tsverifies custom rule registration, node listeners, and rule-context state.test/lint-text.test.tscovers the default recommended config, suggestions, inline disables, severity overrides, emitted declarations, and starter-rule coverage.test/rules/contains focused behavioral tests for individual built-in rules.
Linter Flow
The linter currently works in this order:
- Parse template text with
Document.fromText(...). - Load the effective config by merging the recommended defaults with any provided config.
- Collect inline
antlers-lint-disable ...directives and attach them to the next lintable node. - Instantiate enabled rule listeners with a shared traversal state.
- Walk the Antlers AST depth-first and invoke matching listeners such as
enter,exit, orVariableNode. - Return sorted diagnostics with severity, message, source range, source snippet, and optional suggestions/fixes.
CLI
The package ships an antlers-lint binary that works with npx in CI:
npx antlers-lint [options] [files/globs]Options:
| Flag | Description |
| --------------------- | -------------------------------------------------------------------- |
| --config <path> | Path to a .json or ESM .js config file |
| --fix | Rewrite files by applying every non-overlapping auto-fix |
| --format <reporter> | Output reporter: stylish (default), json, or github-actions |
| --quiet | Suppress warnings and only surface errors |
| --max-warnings <n> | Exit 1 when warnings exceed <n> (default: no threshold) |
| --no-config | Ignore any discovered config file and use recommendedConfig |
| --ignore-pattern <p> | Glob pattern to exclude from linting (repeatable) |
| --no-default-ignore | Disable built-in ignore patterns (vendor, node_modules) |
| --version | Print the installed antlers-linter version |
| -h, --help | Print CLI usage |
Behaviour:
- File discovery uses globs; the default pattern is
resources/views/**/*.antlers.html. - Positional arguments override the default and accept any mix of globs or explicit paths.
- Discovered files matching a built-in ignore (
**/vendor/**,**/node_modules/**), a configignorePatternsentry, or a--ignore-patternvalue are skipped. Pass--no-default-ignoreto lint vendored templates. - When
--fixresolves every problem the file is rewritten in place; otherwise the remaining diagnostics are reported. --format jsonemits an ESLint-compatible array of{ filePath, messages, errorCount, warningCount, ... }records.--format github-actionsemits::error,::warning, and::noticeworkflow commands that GitHub annotates inline on pull requests.
Exit codes:
| Code | Meaning |
| ---- | --------------------------------------------------------------- |
| 0 | Clean (warnings allowed unless --max-warnings is exceeded) |
| 1 | Lint errors found, or warnings exceeded --max-warnings |
| 2 | Configuration or runtime problem (bad flags, unreadable config) |
Example GitHub Actions step:
- name: Lint Antlers templates
run: npx antlers-lint --format github-actions --max-warnings 0Public API
The package exports:
builtinRulesrecommendedConfigDEFAULT_IGNORE_PATTERNS— built-in ignore globs applied by the CLIcreateLinter()lintText()loadConfig()applyFixes()— apply diagnostic fixes to a source stringformatResults(),FORMATTER_NAMES,isFormatterName()— reporter helpersrun(),parseCliArgs(),DEFAULT_PATTERN— CLI helpers for custom integrations
Example:
import { lintText } from 'antlers-linter';
const diagnostics = lintText('{{ get:name }} {{ old:email }}');Configuration
loadConfig() looks for configuration in this order:
antlerslint.config.js.antlerslintrc.jsonpackage.jsonunderantlersLintorantlers-lint
Supported severities are:
offerrorwarninginfohint
The current recommended config is:
{
rules: {
'no-debug-tags': 'error',
'no-duplicate-params': 'warning',
'no-empty-conditionals': 'warning',
'no-nested-loops-depth': 'warning',
'no-unclosed-tags': 'warning',
'no-undefined-partial': 'warning',
'no-unmatched-conditionals': 'warning',
'no-unsanitized-request': 'warning',
'no-raw-user-input': 'warning',
'prefer-ternary': 'warning',
'tag-spacing': 'warning',
'valid-modifier-chain': 'warning'
}
}Rule settings can be provided as:
- a severity string, such as
'warning' - a tuple, such as
['warning', { ...options }] - an object with
severityplus rule-specific options
Ignoring files
Configs can declare additional ignorePatterns to skip vendored or generated
templates alongside the built-in defaults (**/vendor/**, **/node_modules/**):
{
"ignorePatterns": ["storage/**", "public/build/**"]
}Built-in defaults are always applied unless --no-default-ignore is passed on
the CLI. Config-level and CLI patterns are additive.
Inline Disables
The linter supports inline disable directives inside Antlers comments:
{{# antlers-lint-disable no-raw-user-input #}}
{{ old:email }}The directive applies to the next lintable node. It also supports all as a rule id.
Built-in Rules
The package currently ships 13 built-in rules. recommendedConfig enables 12 of them by default; use-disambiguation is available but opt-in.
no-debug-tags
Flags debug helpers that should not ship in committed template source.
Current behavior:
- Recommended severity:
error - Default blocked tags:
dump,dd,debug - Ignores non-debug tags and closing tags
Example:
{{ dump }}
{{ dd }}
{{ debug }}no-duplicate-params
Flags duplicate parameters that appear more than once on the same tag.
Current behavior:
- Recommended severity:
warning - Reports each repeated parameter after its first occurrence on the same tag
- Only compares parameters within a single tag
Example:
{{ partial:card src="hero" src="thumb" }}no-empty-conditionals
Flags if and unless chains whose branches contain no rendered content.
Current behavior:
- Recommended severity:
warning - Checks opening
ifandunlessnodes that are part of a matched conditional chain - Treats
else,elseif, andelseunlessas part of the same chain - Reports only when every branch in the chain is empty
Example:
{{ if featured }}{{ /if }}no-nested-loops-depth
Flags inline expressions whose nested ternary depth exceeds the configured maximum.
Current behavior:
- Recommended severity:
warning - Default
maxDepth:2 - Only checks inline runtime expressions, not Antlers tag nodes or conditional tag nodes
- Reports when nested ternaries are deep enough that the expression should be extracted
Example:
{{ (a ? (b ? (c ? d : e) : f) : g) }}no-raw-user-input
Flags rendered flashed input values that start with old: when they are output without an escaping modifier.
Current behavior:
- Recommended severity:
warning - Default target:
old: - Default accepted modifiers:
entities,sanitize,escape,esc,e - Default suggested modifier:
entities - Only checks rendered variables, not tag expressions or condition expressions
- Suggests appending the preferred escape modifier
Example:
{{ old:email }}no-unclosed-tags
Flags paired Antlers tags that are opened but never closed.
Current behavior:
- Recommended severity:
warning - Only checks tag nodes that are not self-closing and are not conditionals
- Ignores tags already matched with a closing pair
- Leaves unmatched conditionals to
no-unmatched-conditionals
Example:
{{ collection:blog }}
{{ title }}no-undefined-partial
Flags {{ partial:name }} tags that reference a view missing from resources/views.
Current behavior:
- Recommended severity:
warning - Resolves partial names from the current lint
cwd - Supports both slash and dot notation when resolving view paths
- Checks common view extensions such as
.antlers.html,.blade.php,.html, and.php
Example:
{{ partial:hero }}no-unmatched-conditionals
Flags if, elseif, and else conditionals that never resolve to a closing {{ /if }}.
Current behavior:
- Recommended severity:
warning - Reports unmatched opening
ifnodes - Reports orphan
elseifandelsebranches - Walks conditional chains via the parser's matched-tag links
Example:
{{ if featured }}
{{ title }}no-unsanitized-request
Flags rendered request variables that start with get: or post: when they are output without a sanitizing modifier.
Current behavior:
- Recommended severity:
warning - Default targets:
get:,post: - Default accepted modifier:
sanitize - Only checks rendered variables, not tag expressions or condition expressions
- Suggests appending
| sanitize
Example:
{{ get:name }}
{{ post:email }}prefer-ternary
Suggests inline ternaries for simple if/else blocks.
Current behavior:
- Recommended severity:
warning - Only matches
if ... else ... /ifchains - Requires both branches to be standalone Antlers expressions
- Provides a suggestion that rewrites the whole block into
{{ condition ? truthy : falsey }}
Example:
{{ if featured }}{{ title }}{{ else }}{{ fallback_title }}{{ /if }}tag-spacing
Enforces consistent whitespace just inside {{ and }}.
Current behavior:
- Recommended severity:
warning - Default style:
always - Supports
alwaysandnever - Only normalizes the padding adjacent to the delimiters
- Ignores multiline Antlers nodes so layout-heavy tags are not collapsed into one line
Example:
{{ title }}
{{ collection:blog }}
{{ /collection:blog }}use-disambiguation
Requires explicit disambiguation prefixes for rendered variables, conditional variables, and non-conditional tags.
Current behavior:
- Not enabled by
recommendedConfig; choose a severity explicitly to opt in - Requires
$for rendered variables and variables used in condition expressions - Requires
%for non-conditional tags and paired closing tags - Ignores condition tag names such as
ifand/if - Provides insertion fixes for both variable and tag prefixes
Example:
{{ $title }}
{{ if $songs }}{{ /if }}
{{ %collection:blog }}{{ /%collection:blog }}valid-modifier-chain
Flags unknown modifier names in a chain so misspellings and unconfigured custom modifiers do not silently pass through linting.
Current behavior:
- Recommended severity:
warning - Checks modifier chains attached to parsed Antlers runtime nodes
- Ignores modifiers resolved by the parser and any names listed in
additionalModifiers - Reports each unknown modifier once
Example:
{{ title | upper | uppre }}Rule Options
These built-in rules currently accept options:
no-debug-tags
{
rules: {
'no-debug-tags': ['error', {
tagNames: ['dump', 'dd', 'debug', 'clockwork']
}]
}
}no-nested-loops-depth
{
rules: {
'no-nested-loops-depth': ['warning', {
maxDepth: 3
}]
}
}no-unsanitized-request
{
rules: {
'no-unsanitized-request': ['warning', {
targets: ['get:', 'post:'],
sanitizeModifiers: ['sanitize']
}]
}
}no-raw-user-input
{
rules: {
'no-raw-user-input': ['warning', {
targets: ['old:'],
escapeModifiers: ['entities', 'sanitize', 'escape', 'esc', 'e'],
preferredEscapeModifier: 'entities'
}]
}
}tag-spacing
{
rules: {
'tag-spacing': ['warning', {
style: 'always' // or 'never'
}]
}
}valid-modifier-chain
{
rules: {
'valid-modifier-chain': ['warning', {
additionalModifiers: ['markdownify']
}]
}
}Test Suite
The repository currently has:
- runtime tests in
test/ - focused rule specs in
test/rules/ - a public API type smoke test in
type-tests/public-api.ts
The runtime suite covers:
- config loading from
.antlerslintrc.json,antlerslint.config.js, andpackage.json - custom rule registration and listener execution
- default
lintText()behavior, inline disables, severity overrides, and fix suggestions - emitted declaration files in
dist/ - focused rule behavior for
no-debug-tags,no-duplicate-params,no-empty-conditionals,no-nested-loops-depth,no-unclosed-tags,no-undefined-partial,no-unmatched-conditionals,prefer-ternary,tag-spacing,use-disambiguation, andvalid-modifier-chain
Coverage for no-unsanitized-request and no-raw-user-input currently lives in test/lint-text.test.ts, which exercises the recommended defaults and suggestion behavior for those rules.
Development
Run the linter with:
npm run lintRun the full test suite with:
npm testnpm test runs:
npm run buildto emitdist/npm run test:typesto compile the public API smoke test intype-tests/public-api.tsnode --testto execute the runtime suite intest/
Build the package output manually with:
npm run build