Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Why was this comment kept?

A run that reports fewer comments than you expected has not lost them: it decided to keep them, and it can say which rule decided. --explain lists every comment a human check or scan met, the kept ones included, and puts the rule and the setting behind that rule on the line under each one.

--explain annotates a report of comments, so it belongs to the two commands that write one. Asking for it with --format json, or any other machine format, or with a command that writes no report of comments, is a usage error rather than a flag that quietly does nothing.

Ask the binary

This project has a generated directory, a JavaScript file with a lint directive, and a house rule that a NOTE: comment is deliberate:

version = 1

[policy]
mode = "conservative"
keep_regex = ['^//\s*NOTE\b']

[[overrides]]
paths = ["gen/**"]
policy = "all"
keep_regex = ["(?i)generated"]
.ocomment.toml
gen/api.rs
src/app.js
src/lib.rs

--explain names the rule and the setting behind every comment it meets — 0 comments in this project:

$ ocomment check --explain

  NO  1 comment in 1 file · 2 of 3 files scanned · policy conservative

  DECIDE  do what it promises, or delete it                1 comment
    src/app.js:2
      - console.log(1); // TODO: drop
        removed: policy `conservative` removes ordinary comments ([policy] in .ocomment.toml)
    or keep them  [policy.allow]
                  tags = ["TODO"]

  ALLOWED 2 comments this run did not report
    src/app.js:1  // eslint-disable-next-line no-console
      kept: tool or language directive `eslint`; use --remove-kind directive or --policy all to remove it
    src/lib.rs:1  // NOTE: the retry budget is what the server documents.
      kept: matched keep_regex #0 `^//\s*NOTE\b` ([policy] in .ocomment.toml)

  ──────────────────────────────────────────────────────────────────────
   ocomment fix   removes all 1, including anything above you meant to keep

Found 1 removable comment in 1 file (2 files scanned). Run `ocomment fix` to remove it. 1 file skipped (generated file: 1; use -v to list).

Read the second line of each pair as the answer. A keep_regex match kept the first comment even though [[overrides]] puts the whole gen/** tree under the all policy; a marker the scanner recognises kept the lint directive with no configuration at all; and the setting in brackets is named where it was written, so [policy] in .ocomment.toml and [[overrides]] #0 point at the two places that decided this run. A comment no setting decided is left with the flag that would overrule the built-in rule instead.

What each policy removes

spec/directives.toml is the shared table both implementations are checked against, and this is that table. --policy and [policy] mode choose a column, and the settings in the next sections move a single comment out of the column its kind lands in.

Comment kindnoneconservativestandardall
linekeptremovedremovedremoved
blockkeptremovedremovedremoved
doc-linekeptremovedremovedremoved
doc-blockkeptremovedremovedremoved
licensekeptkeptremovedremoved
directivekeptkeptkeptremoved
load-bearingkeptkeptkeptkept unless --force-protected
html-commentkeptkeptkeptremoved
shebangkeptkeptkeptkept unless --force-protected
encodingkeptkeptkeptkept unless --force-protected
optimizer-hintkeptkeptkeptkept unless --force-protected
version-commentkeptkeptkeptkept unless --force-protected

Markers that survive a removal

Some comments are not commentary at all: a build tag, a lint control, or an optimiser hint changes what a compiler, a linter, or a database does with the file. Each row below is scanned by the binary that built this page, so the table cannot claim a protection that is not there.

The Kept because column says which of two protections a marker has, and the difference is what --policy all does to it. A marker kept as a tool or language directive is addressed to something that reports on the code – a linter, a formatter, a coverage tool – so losing it makes that tool noisier and leaves the program alone, and all is free to take it. A marker kept as required by the language or its build is read by the language itself, by its compiler or by its package manager, and losing it changes what compiles or what the code does: //go:build linux decides whether the file is compiled at all, and // swift-tools-version: decides whether a Package.swift is a manifest. No policy is offered that choice, and --force-protected is the only way to give one up.

MarkerLanguageWritten asKindKept because
go:go//go:build linuxload-bearingrequired by the language or its build
+buildgo// +build linuxload-bearingrequired by the language or its build
triple-slash-referencetypescript/// <reference path="types.d.ts" />load-bearingrequired by the language or its build
syntax=shell# syntax=docker/dockerfile:1load-bearingrequired by the language or its build
frozen_string_literal:ruby# frozen_string_literal: trueload-bearingrequired by the language or its build
warn_indent:ruby# warn_indent: trueload-bearingrequired by the language or its build
shareable_constant_value:ruby# shareable_constant_value: literalload-bearingrequired by the language or its build
@dartdart// @dart = 2.12load-bearingrequired by the language or its build
swift-tools-version:swift// swift-tools-version:5.9load-bearingrequired by the language or its build
//> usingscala//> using scala "3.3.0"load-bearingrequired by the language or its build
optimizer-hintoracle/*+ index(t) */optimizer-hintrequired by the language or its build
version-commentmysql/*!40101 SET NAMES utf8 */version-commentrequired by the language or its build
webpackjavascript/* webpackChunkName: "x" */load-bearingrequired by the language or its build
vite-ignorejavascript/* @vite-ignore */load-bearingrequired by the language or its build
#__PURE__javascript/*#__PURE__*/load-bearingrequired by the language or its build
@__PURE__javascript/*@__PURE__*/load-bearingrequired by the language or its build
#__NO_SIDE_EFFECTS__javascript/*#__NO_SIDE_EFFECTS__*/load-bearingrequired by the language or its build
shebangshell#!/bin/shshebangrequired source preamble
encodingpython# -*- coding: utf-8 -*-encodingrequired source preamble
sourceMappingURLjavascript//# sourceMappingURL=bundle.js.mapdirectivetool or language directive
sourceURLjavascript//# sourceURL=bundle.jsdirectivetool or language directive
lint-and-formatterjavascript// eslint-disable-next-line no-evaldirectivetool or language directive
type-checkerpython# type: ignoredirectivetool or language directive
hadolintshell# hadolint ignore=DL3018directivetool or language directive
:schematoml#:schema https://example.test/pyproject.jsondirectivetool or language directive
taplo:toml# taplo: array_auto_expand = falsedirectivetool or language directive
---@diagnosticlua---@diagnostic disable-next-line: undefined-globaldirectivetool or language directive
luacheck:lua-- luacheck: ignore 212directivetool or language directive
selene:lua-- selene: allow(unused_variable)directivetool or language directive
stylua:lua-- stylua: ignoredirectivetool or language directive
luacov:lua-- luacov: disabledirectivetool or language directive
yaml-language-server:yaml# yaml-language-server: $schema=https://example.test/schema.jsondirectivetool or language directive
yamllintyaml# yamllint disable-line rule:line-lengthdirectivetool or language directive
renovate:yaml# renovate: datasource=docker depName=alpinedirectivetool or language directive
checkov:skipyaml# checkov:skip=CKV_AWS_20:public by designdirectivetool or language directive
trivy:ignoreyaml# trivy:ignore:AVD-AWS-0089directivetool or language directive
nosecyaml# nosecdirectivetool or language directive
kics-scanyaml# kics-scan ignore-linedirectivetool or language directive
@schemayaml# @schema type: stringdirectivetool or language directive
phpcs:php// phpcs:ignore Squiz.Commenting.FunctionCommentdirectivetool or language directive
@phpstan-ignorephp// @phpstan-ignore-next-linedirectivetool or language directive
@psalm-suppressphp/** @psalm-suppress InvalidReturnType */directivetool or language directive
@codeCoverageIgnorephp// @codeCoverageIgnoreStartdirectivetool or language directive
rubocop:ruby# rubocop:disable Style/Documentationdirectivetool or language directive
standard:ruby# standard:disable Style/StringLiteralsdirectivetool or language directive
typed:ruby# typed: strictdirectivetool or language directive
zig fmt:zig// zig fmt: offdirectivetool or language directive
styler:r# styler: offdirectivetool or language directive
nocovr# nocov startdirectivetool or language directive
dart formatdart// dart format offdirectivetool or language directive
ignore:dart// ignore: unused_local_variabledirectivetool or language directive
ignore_for_file:dart// ignore_for_file: unused_importdirectivetool or language directive
swiftlint:swift// swiftlint:disable force_castdirectivetool or language directive
swiftformat:swift// swiftformat:disable redundantSelfdirectivetool or language directive
swift-format-ignoreswift// swift-format-ignoredirectivetool or language directive
<auto-generatedcsharp// <auto-generated/>directivetool or language directive
ReSharpercsharp// ReSharper disable once UnusedMember.Localdirectivetool or language directive
csharpier-ignorecsharp// csharpier-ignoredirectivetool or language directive
formatter:java// @formatter:offdirectivetool or language directive
nosonarjava// NOSONARdirectivetool or language directive
pylint:python# pylint: disable=invalid-namedirectivetool or language directive
pragma:python# pragma: no coverdirectivetool or language directive
$non-nlsjava//$NON-NLS-1$directivetool or language directive
checkstyle:java// CHECKSTYLE:OFFdirectivetool or language directive
no criticperl## no criticdirectivetool or language directive
use criticperl## use criticdirectivetool or language directive
cppcheck-suppressc// cppcheck-suppress nullPointerdirectivetool or language directive
lint:ignorego//lint:ignore SA1000 the pattern is checkeddirectivetool or language directive
format:scala// format: offdirectivetool or language directive

--remove-kind directive or --policy all removes a directive anyway. A shebang and an encoding preamble need --force-protected on top of --policy all, because removing one changes how the file is executed or decoded rather than how it reads.

The one keep no setting reaches

Every rule above is about what a comment says. One is about where it sits, and it is the only keep no flag overrules:

k: |
  a
# ends the block
  # yamllint disable
z: 1

A YAML block scalar decides where its body ends from the lines below it, so # ends the block is not commentary: it is what terminates the body, and the directive under it is indented deep enough to be content of that body. Remove the terminating line – and a removal there takes the whole line, which is the least it can take – and the directive rejoins the scalar, so k changes from a to two lines. No removal preserves the value, so the comment stays, and --explain writes the reason under it:

k.yaml:3:1: kept line comment: # ends the block
    kept: it separates a `yaml` block scalar from the kept comment below it; the comment under it has to go first

--policy all is not a way out: it removes the directive as well, and with nothing left standing under the body both comments go. What holds the first one in place is whatever comment survives under it, so that is the line to take first. A surviving comment shallower than the body’s own content ends the scalar on its own and keeps nothing above it – see Languages for the depth this is measured at.

Keeping more

version = 1

[policy]
keep_kind = ["doc-line", "doc-block"]
keep_regex = ['^(//|\(\*|/\*|#)\s*(NOTE|SAFETY|INVARIANT|PERF)\b']

[[overrides]]
paths = ["vendor/**"]
policy = "conservative"

keep_kind names whole kinds from the first column of the table above, so keep_kind = ["doc-line", "doc-block"] takes documentation out of reach of every policy. keep_regex matches the comment token itself, delimiters included, which is what lets ^// anchor a rule to the start of the comment; this repository uses exactly that to require a tag on every explanatory comment it keeps. Both are lists, and --explain reports the index it matched, so keep_regex #0 is the first entry.

That the pattern is matched against the whole token is the detail worth reading twice, because a pattern written against the text inside the comment silently protects nothing: ^\s*rustfmt:: anchors in front of a // that is always there and can never match. A setting that protects nothing is the one failure that looks like success, so a run that walks a directory names every keep_kind, remove_kind, keep_regex and remove_regex that met no comment:

$ ocomment check
keep_regex #0 `^\s*rustfmt::` matched none of the 2 comments this run scanned; it is set in [policy] in .ocomment.toml
A pattern is matched against the whole comment token, so `^` is the comment's own first byte — the `//`, `#` or `/*` — and not the text after it.

The report goes to standard error beside the summary, so a --format json consumer keeps a clean pipe, and -q drops it with every other note. A run over named files stays quiet: a walk is the caller saying everything under here, so a pattern that met nothing in it is a pattern doing no work, while a run over one file is a question about that file and a pattern with nothing to say about it has not thereby failed.

The same walk names an [[overrides]] block whose globs matched no file. A pattern that protects nothing is one failure that looks like success; a path glob that matches nothing is the other, and it is the one that fails in the direction of removing more. An override is how a project exempts files from a rule it keeps everywhere else, so a glob a character off the name of a file sitting right there leaves that rule in force over exactly the files somebody decided it should not cover:

$ ocomment check
[[overrides]] #0 (`.gitignor`) matched none of the 3 files this run reached, so everything it sets was left unapplied

Keeping less

--remove-kind is the mirror of keep_kind and removes a kind the policy would have kept. --policy all removes every kind at once. --force-protected is needed on top of it for a shebang or an encoding preamble. --force-invalid edits the part of a file that scanned when the rest of it did not, and is the only one of these that is about a broken file rather than about a policy. It stops at the failure: past that point the scanner is guessing where tokens end, and the bytes it calls a comment may be code. The verdicts on those comments still stand in the report – they are removable, and they are still in the file, which is what a file that does not lex earns.

When the answer is still surprising

ocomment config explain prints the resolved configuration and where each value came from, naming every kind and pattern it resolved with the index the reports above count from, ocomment doctor reports the environment around it, and ocomment scan --format json gives the span, the line, the column and the text of every comment for a tool to read.

--explain and --trace answer different questions

--explain is about one comment: the rule that decided it and the setting behind that rule, printed under the finding it belongs to. It is part of the report, so it goes to standard output and only the two commands that write a report of comments accept it.

--trace is about the run: which layer of configuration applied, what evidence chose each file’s language, which files were never scanned and why, what was decided for every comment, and which edits were planned from those decisions. It is a diagnostic rather than a product, so it goes to standard error and every command accepts it.

That separation is what lets the two be combined with anything else: --trace json beside --format json leaves the document on standard output byte-for-byte identical to the one the same run writes without it. Standard error also carries the run summary, so a reader that needs every line to parse should add --quiet:

$ ocomment check --quiet --trace json 2>trace.jsonl >/dev/null
$ head -2 trace.jsonl
{"event":"config-resolved","root":"/repo","sources":["built-in defaults"]}
{"event":"file-detected","path":"src/main.rs","language":"rust","dialect":"standard","how":"extension","bytes":18}

The stream is described by spec/trace.schema.json. What it does not record is the scanner’s recursion into an embedded language — a <script> body read as JavaScript, a Markdown fence read as the language its info string names. Those comments are reported at their byte span in the outer file, as they are everywhere else.