ccppccpp home
Browse the documentation

Plugins · CPO

Code rules

Rules over syntax trees and configuration keys: tree-sitter queries, ast-grep patterns and the config check.

These checks come with the ccpp CLI and work on file.write. The grammar is chosen from the path.

tree-sitter

.claude/policies/no-any.policy.md
---
description: No any in TypeScript
on: [file.write]
when:
  tree-sitter:
    query: '((predefined_type) @any (#eq? @any "any"))'
    language: [typescript, tsx]   # optional
    onlyNew: true                 # optional: only what the write introduces
---
No `any` in {{ file.relative }} at {{ facts.line }}:{{ facts.column }} ({{ facts.locations }}).

A query in tree-sitter's S-expression syntax, predicates included (#eq?, #match?, #any-of?…). A finding is reported at the @match capture when the query names one, else at the match's first capture. The check holds when a finding remains.

ast-grep

when: { ast-grep: { language: TypeScript, rule: { pattern: "ipcRenderer.$M($$$)" } } }

{ language, rule }, the rule object of ast-grep YAML (pattern, kind, has, inside…), with the same onlyNew / ranges options.

Builtins

ts-no-any (onlyNew defaults to true) and comments, whose facts.comments lists the comments for where.

Before and after

  • onlyNew: true compares findings by fingerprint: pattern, node type, captured texts and enclosing named definitions (class_declaration:A/method_definition:m). Line shifts and edits elsewhere keep a finding; a second any in the same function is new.
  • ranges: changed keeps findings that touch the edited text: the file is re-parsed incrementally and compared with tree-sitter's changed ranges.

Languages

language lists the grammars a rule is written for. Files of other languages are not concerned. Without language, a file with no grammar skips, and so does content unknown before the write (a shell redirection, checked again once the command ran).

An HTML page, and a .vue, .svelte or .astro component, is also checked on the code it embeds when language lists that code's grammar: <script>, <style>, style="", on* handlers and javascript: URLs. embedded: false opts out.

ccpp cpo lint compiles every query. Syntax errors, unknown node types or fields and unknown predicates (which tree-sitter would silently ignore, matching more) make the policy invalid, with the position in the query.

Facts

language, matches and introduced (findings with line, column, endLine, endColumn, type, text, captures, scope, one-based), count, the line / column / text of the first introduced finding, and locations ("2:10, 3:15").

Configuration files: config

on: [file.write]
when:
  config: { key: "services.*.privileged", equals: true }   # equals, in, matches, type, missing, onlyNew

One key model for json (comments allowed), json5, yaml, toml, xml, ini, properties, dotenv and hcl. Every key, containers included, is an entry with its value, type and position.

  • Key patterns: * one segment, ** any number, [n] / [*] list indexes, * inside a segment (dev*).
  • YAML anchors, aliases and merges are resolved; TOML dates stay text; XML attributes are @name and text is #text; HCL blocks are keyed by type then labels.
  • missing: true holds when no entry satisfies the check; with onlyNew, only when the write removes the last one.

CEL functions

codeLanguage(path), parse(text, language), query(tree, source), parseConfig(text, language), configGet(text, language, key), configFind(text, language, key) and readConfig(path).

Edit this page on GitHub