Skip to content

feat: add YAML support#3757

Open
rodbegbie wants to merge 4 commits into
Automattic:masterfrom
rodbegbie:feature/yaml-support-design
Open

feat: add YAML support#3757
rodbegbie wants to merge 4 commits into
Automattic:masterfrom
rodbegbie:feature/yaml-support-design

Conversation

@rodbegbie

Copy link
Copy Markdown

AI disclosure: this PR was written by an AI coding agent (Claude), working with me interactively over the course of a session. I reviewed and directed the work throughout — see the "AI Disclosure" section below for specifics.

Issues

Closes #2700

Description

Adds YAML support to Harper: harper-ls and harper-cli now lint .yaml/.yml files, checking # comments and prose-like scalar values while leaving structural YAML (keys, identifiers, enum-like config values) alone.

This follows the same standalone-crate pattern as harper-python and harper-html (a new harper-yaml crate wired directly into harper-ls/harper-cli), rather than folding into harper-comments.

The three commits build on each other:

  1. harper-yaml crate itselfYamlMasker runs two independent tree-sitter passes (comments, scalar values) over the parsed document and combines the surviving spans. Scalar-value detection has to explicitly exclude mapping keys, since unquoted YAML keys are the same tree-sitter node kind as values — this is done by comparing a candidate node's start byte against its enclosing block_mapping_pair/flow_pair's start byte (a key is always the first token in its pair; a value always starts later, after key:). A small heuristics::is_prose_scalar function decides whether a scalar value looks like prose worth checking (3+ words, not solely a URL/path/version-shaped token) versus structural config data. A small DedentLines parser wrapper avoids a "double space" false positive that raw YAML block-scalar indentation would otherwise trigger.
  2. Wiring into harper-ls/harper-cli — routes "yaml"/.yaml/.yml the same way "python"/.py already routes to PythonParser.
  3. Fuzzing, VS Code activation, and docs — a dedicated fuzz_harper_yaml target (mirroring fuzz_harper_html), onLanguage:yaml activation plus a VS Code integration test, and a docs table entry.

How Has This Been Tested?

  • cargo test --workspace — full workspace suite passes, including 39 new unit/integration tests in harper-yaml covering comments, plain/quoted/block scalars, the spellchecker:ignore-family suppression, mapping-key exclusion, trailing same-line comments, and the URL/path/version/identifier heuristics.
  • just test-vscode — the real VS Code extension integration suite (36 specs) passes, exercising the built harper-ls binary end to end.
  • Manually verified via harper-cli lint against hand-written fixtures.

AI Disclosure

  • I am a human and didn't use any AI.
  • I used LLM features of my editor, but not an agent.
  • I used an AI agent interactively.
  • I am an agent or I got an agent to do the work autonomously.

If Your PR Implements or Enhances a Linter

  • I made up the sentences in the unit tests.
  • The sentences in the unit tests were generated by an AI.
  • I'm using examples from the bug report / feature request.
  • I collected real-world sentences for the unit tests.

Checklist

  • I have performed a self-review of my own code
  • I have added tests to cover my changes
  • I have considered splitting this into smaller pull requests.

rodbegbie and others added 4 commits July 3, 2026 20:53
Adds a new harper-yaml crate, following the same standalone pattern
as harper-python and harper-html rather than folding into
harper-comments. YamlParser lints `#` comments (with the usual
spellchecker-ignore-family suppression) and prose-like scalar values
(plain, quoted, or block scalars), while leaving structural YAML
(keys, identifiers, enum-like values) untouched.

The core pieces:

- YamlMasker runs two independent tree-sitter passes over the parsed
  document -- one for comment nodes, one for scalar-value nodes --
  and combines the surviving spans. Scalar nodes exclude mapping
  keys (unquoted keys are the same tree-sitter node kind as values,
  so a key-vs-value distinction has to be made explicitly by
  comparing a candidate node's start position against its enclosing
  mapping pair's start position).
- heuristics::is_prose_scalar decides whether a scalar value looks
  like prose worth checking, as opposed to structural config data:
  it requires 3+ words and rejects values that are, in their
  entirety, a single URL/path/version-shaped token (so "v1.2.3" is
  skipped, but "upgrade to version 1.2.3" is still checked).
- DedentLines wraps the inner PlainEnglish parser to parse each line
  of a scalar independently after trimming its whitespace, avoiding a
  spurious "double space" false positive that raw multi-line block
  scalar indentation would otherwise trigger.

Closes Automattic#2700.

Entire-Checkpoint: 8eae5ee14317
harper-ls now routes the "yaml" language ID directly to YamlParser,
the same way it already routes "python" to PythonParser. harper-cli
routes .yaml/.yml files the same way it routes .py/.pyi.

Entire-Checkpoint: 173be144ecc2
- New fuzz_harper_yaml target, mirroring fuzz_harper_html.
- VS Code plugin: register onLanguage:yaml activation and add a
  YAML case to the language integration test suite.
- Document YAML in the language-server support table. Marked
  "Comments Only: no" (unlike TOML), since YamlParser also lints
  prose-like scalar values, not just comments.

Entire-Checkpoint: 3d4124766057
Address review findings on the YAML support crate:

- Remove unreachable dead code in `is_prose_scalar`: under the
  `word_count >= 3` gate, `looks_url_path_or_version` (bails at >1 word)
  and `is_snake_or_kebab_case` (bails on whitespace) could never fire.
  Behaviour is unchanged - the word-count gate already subsumes every
  single-word URL/path/version/identifier case - but the code and its
  helpers are gone, so tests no longer imply filtering that isn't there.
- Strip YAML block-scalar indicators (`|`, `>`, `|-`, `>+2`, ...) before
  the word-count gate so a short block-scalar body isn't pushed over the
  prose threshold by the leading indicator token.
- Replace `is_mapping_key`'s byte-position heuristic with tree-sitter's
  named `key` field, so explicit-key syntax (`? key` / `: value`) no
  longer misclassifies the key as a lintable value.
- Coalesce overlapping spans before building the `Mask`, so adversarial
  input cannot trigger the `FromIterator` overlap panic (a DoS in
  harper-ls).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 6c181a01b5d3
@rodbegbie rodbegbie force-pushed the feature/yaml-support-design branch from 68ae54b to 48a4cc0 Compare July 4, 2026 04:44
@hippietrail hippietrail added enhancement New feature or request harper-comments labels Jul 4, 2026

@elijah-potter elijah-potter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good stuff! I just have a few nits.

///
/// Comments are never passed through this function — they're
/// always linted, handled separately in `YamlMasker`.
pub(crate) fn is_prose_scalar(text: &str) -> bool {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The (crate) tag is almost never necessary.

Comment thread harper-yaml/src/dedent.rs
/// incorrectly triggers Harper's "double space" formatting rule.
/// De-denting each line before parsing avoids that, without needing to
/// understand YAML's specific indentation rules.
pub(crate) struct DedentLines<P> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment. The (crate) tag is superfluous.

Comment thread harper-yaml/src/dedent.rs
Comment on lines +4 to +15
/// Wraps a `Parser`, parsing each line of a (possibly multiline) span
/// independently after trimming its leading/trailing whitespace, then
/// stitching the results back together with a single explicit
/// [`TokenKind::Newline`] token between lines.
///
/// This exists for YAML block/folded scalars: their continuation lines
/// carry literal indentation (e.g. two leading spaces). Feeding
/// that raw text straight to a prose parser makes the newline-plus-
/// indentation read as a run of several space characters, which
/// incorrectly triggers Harper's "double space" formatting rule.
/// De-denting each line before parsing avoids that, without needing to
/// understand YAML's specific indentation rules.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This functionality exists for other parsers (i.e. HTML). Would it be possible to centralize the logic? It would make it FAR easier to maintain.

Comment on lines +6 to +9
fuzz_target!(|data: &str| {
let parser = harper_yaml::YamlParser::default();
let _res = parser.parse_str(data);
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious if you've actually run the fuzzer. If so, what were the results?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request harper-comments

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request to support YAML

3 participants