Need more than a rule playground?

Explore Vale CMS
Why Vale

Reads comments with tree-sitter, in 20+ languages

Code-aware

Most of what a developer reads about your software is written inside the software. Vale treats comments and docstrings as first-class prose: it parses the source with a real grammar, pulls the prose out, lints it, and reports every alert at its line and column in the original file.

Source code reference

A grammar draws the boundary

Vale parses the file with tree-sitter and runs a query against the parse tree. A comment is whatever the language's own grammar calls a comment—which is why a delimiter inside a string literal stays where it belongs.

store.go
package store
// Get returns the record for id.
//
// It utilizes the cache when possible.
func Get(id string) (*Record, error) {
key := "// not a comment"
return s.lookup(key + id)
}
Query
(comment) @comment

One query covers line and block comments alike—the grammar already knows the difference.

Highlighted lines are handed to your rules. Everything dimmed is code—including the string literals that contain comment delimiters, which is exactly the case a pattern-matching extractor gets wrong.

The parsing is tree-sitter, through the Go bindings, with one grammar per language and a query naming the nodes to collect. It is the same machinery your editor uses to highlight the file.

Markdown, inside a comment

Doc comments are rarely plain text. Associate a markup format with a file extension and Vale parses the comment body with that format's parser, so everything on the markup page applies inside your source files.

.vale.ini
[formats]
# Rust source, Markdown comments
rs = md
[*.{rs,md}]
BasedOnStyles = Vale, Microsoft
  1. 1

    Find

    tree-sitter locates every comment in the source file.

  2. 2

    Undecorate

    Per-line markers come off, and the body is dedented.

  3. 3

    Parse

    The remaining text goes through the markup format's own parser.

  4. 4

    Map back

    Each alert is reported at its position in the source file.

Once a format is associated, its features come with it—including TokenIgnores and BlockIgnores, which are otherwise unavailable in source code because they work by wrapping a match in the format's code delimiter.

Decoration is not markup

A C-style block comment decorates every line with an asterisk. Markdown reads that asterisk as a list marker. Vale removes the decoration first, so a comment parses as what its author meant.

Source
/**
* Reads the record and returns it.
*
* Pass `refresh` to bypass the cache:
*
* * `refresh: true` re-reads from disk.
* * `refresh: false` uses the cache.
*/
What the parser gets
Reads the record and returns it.
Pass `refresh` to bypass the cache:
* `refresh: true` re-reads from disk.
* `refresh: false` uses the cache.

A paragraph followed by a two-item list—not one seven-item list, which is what the decoration would otherwise produce.

An asterisk counts as decoration only when whitespace or the end of the line follows it, so a line that starts *emphasis* keeps its markup. Relative indentation survives too, which is what makes an indented fenced block inside a comment still read as code.

Requires Vale v3.17.0 or later.

An API spec is documentation

Every summary and description in an OpenAPI document is published prose—it becomes your API reference. Vale reads structured data through a second engine, so those fields can be linted by the same rules as the rest of your docs.

openapi.yaml
openapi: 3.1.0
info:
title: Pet Store API
version: 1.0.0
description: |-
Manage pets, orders, and users.
See [the guide](/guide) to get started.
paths:
/pet:
post:
summary: Add a new pet to the store
operationId: addPet

Highlighted lines are linted. Version numbers, operation IDs, and path keys are structure, and a rule never sees them.

Name the fields

A view is a list of dasel selectors. Give one a name and it becomes a scope you can target.

# styles/config/views/OpenAPI.yml
engine: dasel
scopes:
- name: title
expr: info.title
- expr: info.description
type: md
- expr: paths.all().all().property(summary,description)

Point it at the file

[*.{yaml,json}]
BasedOnStyles = Vale, Microsoft
View = OpenAPI

type: md matters

A description written in Markdown is parsed as Markdown, so the link target and the code span inside it are skipped exactly as they would be in a documentation page. Any markup format works—adoc, rst, html, or org.

The same approach covers anything with prose in it: a Kubernetes chart's descriptions, an Ansible playbook's task names, a GitHub Actions workflow's step names. JSON, YAML, and TOML are all handled by the one engine.

Bring your own query

Views work the other way too. Swap the engine for tree-sitter and the same mechanism replaces the built-in comment query, so you can lint whatever part of a source file you consider prose.

Lint the string literals

User-facing strings are prose your readers see more often than your docs.

# styles/config/views/Strings.yml
engine: tree-sitter
scopes:
- expr: (string_literal)+ @string
name: java

Languages

Fourteen have a tree-sitter grammar built in.

GoRustPythonRubyJavaScriptTypeScriptTSXJavaCC++JuliaProtobufCSSYAML

The rest are handled by delimiter scanning—still comment-aware, but without a parse tree behind it:

C#HaskellLESSLuaPerlPHPPowerShellRSassScalaSwift