From a68b74ea8950f0f2e89ab849a12ccabd775781e6 Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:50:37 +0900 Subject: [PATCH 01/21] feat(cli): stable enum names, full help text, man page, human output without Debug leaks, run summary, -q/-v, comment preview - core: ALL/as_str/aliases/Display on Language, Dialect, CommentKind, Policy, Layout, Severity; Display for Disposition (serde shapes and Keep reasons frozen, pinned by tests/names.rs) - cli: ValueEnum wrappers (values.rs) so --policy/--layout/--language/--dialect/--keep-kind/--remove-kind list and complete their values; descriptions for every subcommand and option; Policy/Output help headings; EXIT STATUS/FILES/EXAMPLES/SEE ALSO; `man` subcommand via clap_mangen; release_extras.py renders the man page from the binary - output: removable/kept labels use kebab-case kind names everywhere (human, scan, diagnostics, SARIF, GitHub, LSP hover, config explain); end-of-run summary on stderr for human format; `fixed ` lines for fix; -q/--quiet and -v/--verbose; directory-walk skips folded into the summary; one-line sanitized comment preview (--no-preview to disable) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RLSWPMQ9PvtGi3jyBF7bJx --- docs/ocomment.1 | 381 ++++- release-extras/_ocomment | 1845 +++++++++++++++++++----- release-extras/_ocomment.ps1 | 747 ++++++---- release-extras/ocomment.1 | 381 ++++- release-extras/ocomment.bash | 490 ++++--- release-extras/ocomment.elv | 745 ++++++---- release-extras/ocomment.fish | 1748 ++++++++++++++++++---- rust/Cargo.lock | 18 + rust/Cargo.toml | 3 +- rust/ocomment-core/src/types.rs | 352 ++++- rust/ocomment-core/tests/names.rs | 395 +++++ rust/ocomment-core/tests/properties.rs | 2 +- rust/ocomment/Cargo.toml | 2 + rust/ocomment/src/cli.rs | 363 ++++- rust/ocomment/src/config.rs | 66 +- rust/ocomment/src/files.rs | 23 +- rust/ocomment/src/git.rs | 30 +- rust/ocomment/src/lsp.rs | 12 +- rust/ocomment/src/main.rs | 1 + rust/ocomment/src/output.rs | 464 +++++- rust/ocomment/src/values.rs | 239 +++ rust/ocomment/tests/cli.rs | 896 ++++++++++++ rust/ocomment/tests/lsp.rs | 61 + tools/release_extras.py | 10 +- 24 files changed, 7484 insertions(+), 1790 deletions(-) create mode 100644 rust/ocomment-core/tests/names.rs create mode 100644 rust/ocomment/src/values.rs diff --git a/docs/ocomment.1 b/docs/ocomment.1 index cffafdb..9ec68fd 100644 --- a/docs/ocomment.1 +++ b/docs/ocomment.1 @@ -1,86 +1,343 @@ -.TH OCOMMENT 1 "August 2026" "ocomment 0.1.0" "User Commands" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.TH OCOMMENT 1 "ocomment 0.1.0" "User Commands" .SH NAME -ocomment \- check and remove source-code comments safely +ocomment \- Check and remove source\-code comments safely .SH SYNOPSIS -.B ocomment -.RI [ PATH ...] -.br -.B ocomment -.RI COMMAND " [OPTIONS]" +\fBocomment\fR [\fB\-\-config\fR] [\fB\-\-policy\fR] [\fB\-\-layout\fR] [\fB\-\-language\fR] [\fB\-\-dialect\fR] [\fB\-\-keep\-kind\fR] [\fB\-\-remove\-kind\fR] [\fB\-\-force\-invalid\fR] [\fB\-\-force\-protected\fR] [\fB\-\-format\fR] [\fB\-\-color\fR] [\fB\-\-hyperlinks\fR] [\fB\-\-no\-preview\fR] [\fB\-\-progress\fR] [\fB\-q\fR|\fB\-\-quiet\fR] [\fB\-v\fR|\fB\-\-verbose\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIPATH\fR] [\fIsubcommands\fR] .SH DESCRIPTION -OComment scans source bytes without requiring UTF-8 and reports or removes -comment tokens. The default policy protects source preambles and tool or -language directives. Rewrites are prepared and committed as one rollback-backed -transaction. -.SH COMMANDS +OComment scans source bytes without requiring UTF\-8 and reports or removes comment tokens. The default policy protects source preambles and tool or language directives. Rewrites are prepared and committed as one rollback\-backed transaction. +.SH OPTIONS .TP -.B check [PATH...] -Report removable comments. This is the default command. +\fB\-\-config\fR \fI\fR +Read this configuration file instead of discovering `.ocomment.toml` .TP -.B fix [PATH...] -Atomically apply safe edits. +\fB\-h\fR, \fB\-\-help\fR +Print help (see a summary with \*(Aq\-h\*(Aq) .TP -.B diff [PATH...] -Print unified differences. +\fB\-V\fR, \fB\-\-version\fR +Print version .TP -.B strip -Read source from standard input and write transformed bytes to standard output. +[\fIPATH\fR] +Files or directories to check (default: current directory) +.SH POLICY .TP -.B scan [PATH...] -Print comment spans, kinds, dispositions, diagnostics, edits, and source maps. +\fB\-\-policy\fR \fI\fR +Which classes of comment the run is allowed to remove +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +safe: Remove ordinary and doc comments; keep preambles and directives +.IP \(bu 2 +legal: Like safe, and keep licence and copyright comments as well +.IP \(bu 2 +all: Remove every comment that no keep override protects +.RE .TP -.B lsp -Run the LSP 3.18 server over stdio. +\fB\-\-layout\fR \fI\fR +How the bytes left behind by a removed comment are laid out +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +lines: Keep the line structure and separate tokens that would otherwise join +.IP \(bu 2 +columns: Pad each removed comment so the following columns do not shift +.IP \(bu 2 +compact: Drop the comment bytes and keep only the newlines it spanned +.RE .TP -.B init, config, languages, plugin, completions, doctor -Manage configuration, integrations, plugins, generated completions, and -environment diagnostics. -.SH OPTIONS +\fB\-\-language\fR \fI\fR +Force this language instead of detecting it from path and contents +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +rust: Rust source files +.IP \(bu 2 +ocaml: OCaml implementation and interface files +.IP \(bu 2 +c: C source and header files +.IP \(bu 2 +cpp: C++ source and header files +.IP \(bu 2 +go: Go source files +.IP \(bu 2 +java: Java source files, including Unicode escape translation +.IP \(bu 2 +javascript: JavaScript modules and scripts, including JSX +.IP \(bu 2 +typescript: TypeScript modules and scripts, including TSX +.IP \(bu 2 +python: Python source and stub files +.IP \(bu 2 +shell: POSIX sh, Bash, and zsh scripts +.IP \(bu 2 +html: HTML documents, including nested script and style elements +.IP \(bu 2 +css: CSS stylesheets +.IP \(bu 2 +jsonc: JSON with comments, including JSON5 +.IP \(bu 2 +sql: SQL for every supported database dialect +.IP \(bu 2 +kotlin: Kotlin source and script files +.RE +.TP +\fB\-\-dialect\fR \fI\fR +Force this dialect of the selected language +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +standard: The default lexical rules of the language +.IP \(bu 2 +jsx: JavaScript with JSX elements +.IP \(bu 2 +tsx: TypeScript with JSX elements +.IP \(bu 2 +objective\-c: Objective\-C extensions to C +.IP \(bu 2 +objective\-cpp: Objective\-C++ extensions to C++ +.IP \(bu 2 +gnu\-c: GNU extensions to C +.IP \(bu 2 +gnu\-cpp: GNU extensions to C++ +.IP \(bu 2 +cuda: CUDA extensions to C++ +.IP \(bu 2 +posix\-sh: The POSIX shell command language +.IP \(bu 2 +bash53: Bash 5.3 +.IP \(bu 2 +zsh: The Z shell +.IP \(bu 2 +postgresql: PostgreSQL, with dollar\-quoted bodies +.IP \(bu 2 +mysql: MySQL, including its executable versioned comments +.IP \(bu 2 +sqlite: SQLite +.IP \(bu 2 +t\-sql: Microsoft Transact\-SQL +.IP \(bu 2 +oracle: Oracle SQL and PL/SQL +.RE +.TP +\fB\-\-keep\-kind\fR \fI\fR +Comma\-separated comment kinds to protect on top of the policy +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +line: An ordinary comment running to the end of the line +.IP \(bu 2 +block: An ordinary delimited comment +.IP \(bu 2 +doc\-line: A documentation comment running to the end of the line +.IP \(bu 2 +doc\-block: A delimited documentation comment +.IP \(bu 2 +directive: A tool or language directive such as a pragma or lint control +.IP \(bu 2 +license: A licence or copyright preamble +.IP \(bu 2 +html\-comment: A DOM\-observable HTML comment +.IP \(bu 2 +shebang: The interpreter line starting an executable script +.IP \(bu 2 +encoding: A source encoding declaration +.IP \(bu 2 +optimizer\-hint: A compiler or database optimizer hint +.IP \(bu 2 +version\-comment: A MySQL versioned comment that the server executes +.RE +.TP +\fB\-\-remove\-kind\fR \fI\fR +Comma\-separated comment kinds to remove regardless of the policy +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +line: An ordinary comment running to the end of the line +.IP \(bu 2 +block: An ordinary delimited comment +.IP \(bu 2 +doc\-line: A documentation comment running to the end of the line +.IP \(bu 2 +doc\-block: A delimited documentation comment +.IP \(bu 2 +directive: A tool or language directive such as a pragma or lint control +.IP \(bu 2 +license: A licence or copyright preamble +.IP \(bu 2 +html\-comment: A DOM\-observable HTML comment +.IP \(bu 2 +shebang: The interpreter line starting an executable script +.IP \(bu 2 +encoding: A source encoding declaration +.IP \(bu 2 +optimizer\-hint: A compiler or database optimizer hint +.IP \(bu 2 +version\-comment: A MySQL versioned comment that the server executes +.RE +.TP +\fB\-\-force\-invalid\fR +Apply the edits that are still provably safe when the source fails to scan +.TP +\fB\-\-force\-protected\fR +Remove protected comments such as shebang and encoding preambles +.SH OUTPUT +.TP +\fB\-\-format\fR \fI\fR [default: human] +Output encoding +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +human +.IP \(bu 2 +json +.IP \(bu 2 +jsonl +.IP \(bu 2 +sarif +.IP \(bu 2 +github +.RE +.TP +\fB\-\-color\fR \fI\fR [default: auto] +When to colour terminal output +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +auto +.IP \(bu 2 +always +.IP \(bu 2 +never +.RE +.TP +\fB\-\-hyperlinks\fR \fI\fR [default: auto] +When to emit terminal hyperlinks for reported paths +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +auto +.IP \(bu 2 +always +.IP \(bu 2 +never +.RE +.TP +\fB\-\-no\-preview\fR +Omit the one\-line comment text from human `check` and `scan` lines +.TP +\fB\-\-progress\fR \fI\fR [default: auto] +Accepted for compatibility; the end\-of\-run summary replaced this line +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +auto +.IP \(bu 2 +always +.IP \(bu 2 +never +.RE +.TP +\fB\-q\fR, \fB\-\-quiet\fR +Print nothing but errors and diagnostics +.TP +\fB\-v\fR, \fB\-\-verbose\fR +Trace what is scanned and summarize every comment kind and skipped file +.SH SUBCOMMANDS .TP -.BI --format " human|json|jsonl|sarif|github" -Select output format. +ocomment\-check(1) +Report removable comments (default command) .TP -.BI --policy " safe|legal|all" -Select removal policy. +ocomment\-fix(1) +Remove comments in place through an atomic, rollback\-backed transaction .TP -.BI --layout " lines|columns|compact" -Select replacement layout. +ocomment\-diff(1) +Print a unified diff of the changes fix would make .TP -.BI --language " LANGUAGE" -Override language detection. +ocomment\-scan(1) +List every comment with its kind, disposition and byte span .TP -.BI --dialect " DIALECT" -Select a guaranteed language dialect. +ocomment\-strip(1) +Read source on stdin and write the stripped result to stdout .TP -.BI --keep-kind " KIND" -Keep a comment kind. May be repeated or comma separated. +ocomment\-lsp(1) +Run the LSP 3.18 server over stdio .TP -.BI --remove-kind " KIND" -Remove a comment kind. May be repeated or comma separated. +ocomment\-init(1) +Write a starter .ocomment.toml or Lefthook configuration .TP -.B --force-invalid -Apply known safe spans even when the lexical scan reports invalid input. +ocomment\-config(1) +Show, locate, explain, or export the resolved configuration .TP -.B --force-protected -Allow the all policy to remove protected comment preambles. +ocomment\-languages(1) +List built\-in languages, extensions, and dialects .TP -.B --staged -Operate on comments whose starting delimiter is in a Git index addition. +ocomment\-plugin(1) +Manage sandboxed WASM scanner plugins .TP -.B --index-only -With --staged, update only the Git index. -.SH EXIT STATUS -0 means clean or a successful transformation. 1 means check or diff found a -change. 2 means configuration, I/O, source, staged-mapping, or plugin failure. -.SH FILES +ocomment\-completions(1) +Generate shell completions .TP -.B .ocomment.toml -Project configuration, version 1. +ocomment\-doctor(1) +Diagnose the environment (config, git, plugins, tools) .TP -.B .ocommentignore -Additional ignore patterns. +ocomment\-man(1) +Render the roff manual page to stdout .TP -.B .ocomment.lock -Pinned plugin artifacts and identities. -.SH SEE ALSO -The complete schemas and guides are available in the OComment repository. +ocomment\-help(1) +Print this message or the help of the given subcommand(s) +.SH EXTRA +EXIT STATUS + 0 Nothing removable was found and every requested change was applied. + 1 Removable comments were reported, or a diff was printed. + 2 Invalid source, configuration, plugin, or I/O failure. + +FILES + .ocomment.toml Project configuration, merged over the user file. + .ocommentignore Extra ignore patterns honoured by repository walks. + .ocomment.lock Pinned digests of the installed WASM scanner plugins. + $XDG_CONFIG_HOME/ocomment/config.toml + User configuration, merged over the built\-in defaults. + +EXAMPLES + ocomment + Check the current repository and report removable comments. + ocomment fix \-\-policy all \-\-layout compact src + Remove every comment under src and close the gaps it leaves. + ocomment strip \-\-language rust < before.rs > after.rs + Strip one file from standard input to standard output. + +SEE ALSO + The complete schemas and guides are available in the OComment repository. +.SH VERSION +v0.1.0 diff --git a/release-extras/_ocomment b/release-extras/_ocomment index fd4a1ce..efcdcf6 100644 --- a/release-extras/_ocomment +++ b/release-extras/_ocomment @@ -15,24 +15,82 @@ _ocomment() { local context curcontext="$curcontext" state line _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ '-V[Print version]' \ '--version[Print version]' \ -'::paths -- Paths for the implicit `check` command:_files' \ +'::paths -- Files or directories to check (default\: current directory):_files' \ ":: :_ocomment_commands" \ "*::: :->ocomment" \ && ret=0 @@ -44,207 +102,787 @@ _ocomment() { case $line[2] in (check) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ '--staged[Read and update Git index blobs rather than treating the working tree as the source]' \ '--index-only[With \`--staged\`, do not attempt a uniquely mappable working-tree update]' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'*::paths:_files' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ +'*::paths -- Files or directories to process (default\: current directory):_files' \ && ret=0 ;; (fix) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ '--staged[Read and update Git index blobs rather than treating the working tree as the source]' \ '--index-only[With \`--staged\`, do not attempt a uniquely mappable working-tree update]' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'*::paths:_files' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ +'*::paths -- Files or directories to process (default\: current directory):_files' \ && ret=0 ;; (diff) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ '--staged[Read and update Git index blobs rather than treating the working tree as the source]' \ '--index-only[With \`--staged\`, do not attempt a uniquely mappable working-tree update]' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'*::paths:_files' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ +'*::paths -- Files or directories to process (default\: current directory):_files' \ && ret=0 ;; (scan) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ '--staged[Read and update Git index blobs rather than treating the working tree as the source]' \ '--index-only[With \`--staged\`, do not attempt a uniquely mappable working-tree update]' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'*::paths:_files' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ +'*::paths -- Files or directories to process (default\: current directory):_files' \ && ret=0 ;; (strip) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ && ret=0 ;; (lsp) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ && ret=0 ;; (init) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--fix[]' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'::kind:(config lefthook)' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--fix[For the Lefthook hook, run \`fix\` instead of \`check\`]' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ +'::kind -- Which starter file to write:(config lefthook)' \ && ret=0 ;; (config) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'::action:(show locate explain schema)' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ +'::action -- Which view of the resolved configuration to print:(show locate explain schema)' \ && ret=0 ;; (languages) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ && ret=0 ;; (plugin) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ ":: :_ocomment__subcmd__plugin_commands" \ "*::: :->plugin" \ && ret=0 @@ -257,124 +895,472 @@ _arguments "${_arguments_options[@]}" : \ case $line[1] in (add) _arguments "${_arguments_options[@]}" : \ -'--name=[]:NAME:_default' \ -'--sha256=[]:SHA256:_default' \ -'--identity=[]:IDENTITY:_default' \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--name=[Name to register the plugin under (default\: the file stem)]:NAME:_default' \ +'--sha256=[Expected SHA-256 digest of the component, verified before install]:HEX:_default' \ +'--identity=[Publisher identity recorded alongside the pinned digest]:IDENTITY:_default' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -':source:_default' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ +':source -- Path or URL of the WASM component to install:_default' \ && ret=0 ;; (remove) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -':name:_default' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ +':name -- Name of the plugin to remove:_default' \ && ret=0 ;; (list) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ && ret=0 ;; (update) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'::name:_default' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ +'::name -- Name of the plugin to update (default\: all of them):_default' \ && ret=0 ;; (verify) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -'::name:_default' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ +'::name -- Name of the plugin to verify (default\: all of them):_default' \ && ret=0 ;; (new) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -':path:_files' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ +':path -- Directory to create the plugin crate in:_files' \ && ret=0 ;; (help) @@ -427,41 +1413,234 @@ esac ;; (completions) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ -':shell:(bash elvish fish powershell zsh)' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ +':shell -- Shell whose completion script is written to stdout:(bash elvish fish powershell zsh)' \ && ret=0 ;; (doctor) _arguments "${_arguments_options[@]}" : \ -'--config=[Explicit configuration file]:CONFIG:_files' \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ -'--policy=[]:POLICY:_default' \ -'--layout=[]:LAYOUT:_default' \ -'--language=[]:LANGUAGE:_default' \ -'--dialect=[]:DIALECT:_default' \ -'*--keep-kind=[]:KEEP_KIND:_default' \ -'*--remove-kind=[]:REMOVE_KIND:_default' \ -'--color=[]:COLOR:(auto always never)' \ -'--hyperlinks=[]:HYPERLINKS:(auto always never)' \ -'--progress=[]:PROGRESS:(auto always never)' \ -'--force-invalid[]' \ -'--force-protected[]' \ -'-h[Print help]' \ -'--help[Print help]' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ +&& ret=0 +;; +(man) +_arguments "${_arguments_options[@]}" : \ +'--config=[Read this configuration file instead of discovering \`.ocomment.toml\`]:FILE:_files' \ +'--policy=[Which classes of comment the run is allowed to remove]:POLICY:((safe\:"Remove ordinary and doc comments; keep preambles and directives" +legal\:"Like safe, and keep licence and copyright comments as well" +all\:"Remove every comment that no keep override protects"))' \ +'--layout=[How the bytes left behind by a removed comment are laid out]:LAYOUT:((lines\:"Keep the line structure and separate tokens that would otherwise join" +columns\:"Pad each removed comment so the following columns do not shift" +compact\:"Drop the comment bytes and keep only the newlines it spanned"))' \ +'--language=[Force this language instead of detecting it from path and contents]:LANGUAGE:((rust\:"Rust source files" +ocaml\:"OCaml implementation and interface files" +c\:"C source and header files" +cpp\:"C++ source and header files" +go\:"Go source files" +java\:"Java source files, including Unicode escape translation" +javascript\:"JavaScript modules and scripts, including JSX" +typescript\:"TypeScript modules and scripts, including TSX" +python\:"Python source and stub files" +shell\:"POSIX sh, Bash, and zsh scripts" +html\:"HTML documents, including nested script and style elements" +css\:"CSS stylesheets" +jsonc\:"JSON with comments, including JSON5" +sql\:"SQL for every supported database dialect" +kotlin\:"Kotlin source and script files"))' \ +'--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" +jsx\:"JavaScript with JSX elements" +tsx\:"TypeScript with JSX elements" +objective-c\:"Objective-C extensions to C" +objective-cpp\:"Objective-C++ extensions to C++" +gnu-c\:"GNU extensions to C" +gnu-cpp\:"GNU extensions to C++" +cuda\:"CUDA extensions to C++" +posix-sh\:"The POSIX shell command language" +bash53\:"Bash 5.3" +zsh\:"The Z shell" +postgresql\:"PostgreSQL, with dollar-quoted bodies" +mysql\:"MySQL, including its executable versioned comments" +sqlite\:"SQLite" +t-sql\:"Microsoft Transact-SQL" +oracle\:"Oracle SQL and PL/SQL"))' \ +'*--keep-kind=[Comma-separated comment kinds to protect on top of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'*--remove-kind=[Comma-separated comment kinds to remove regardless of the policy]:KIND:((line\:"An ordinary comment running to the end of the line" +block\:"An ordinary delimited comment" +doc-line\:"A documentation comment running to the end of the line" +doc-block\:"A delimited documentation comment" +directive\:"A tool or language directive such as a pragma or lint control" +license\:"A licence or copyright preamble" +html-comment\:"A DOM-observable HTML comment" +shebang\:"The interpreter line starting an executable script" +encoding\:"A source encoding declaration" +optimizer-hint\:"A compiler or database optimizer hint" +version-comment\:"A MySQL versioned comment that the server executes"))' \ +'--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ +'--color=[When to colour terminal output]:WHEN:(auto always never)' \ +'--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ +'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ +'--force-protected[Remove protected comments such as shebang and encoding preambles]' \ +'--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ +'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ +'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'-v[Trace what is scanned and summarize every comment kind and skipped file]' \ +'--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ +'-h[Print help (see more with '\''--help'\'')]' \ +'--help[Print help (see more with '\''--help'\'')]' \ && ret=0 ;; (help) @@ -560,6 +1739,10 @@ _arguments "${_arguments_options[@]}" : \ _arguments "${_arguments_options[@]}" : \ && ret=0 ;; +(man) +_arguments "${_arguments_options[@]}" : \ +&& ret=0 +;; (help) _arguments "${_arguments_options[@]}" : \ && ret=0 @@ -576,18 +1759,19 @@ esac (( $+functions[_ocomment_commands] )) || _ocomment_commands() { local commands; commands=( -'check:' \ -'fix:' \ -'diff:' \ -'scan:' \ -'strip:' \ -'lsp:' \ -'init:' \ -'config:' \ -'languages:' \ -'plugin:' \ -'completions:' \ -'doctor:' \ +'check:Report removable comments (default command)' \ +'fix:Remove comments in place through an atomic, rollback-backed transaction' \ +'diff:Print a unified diff of the changes fix would make' \ +'scan:List every comment with its kind, disposition and byte span' \ +'strip:Read source on stdin and write the stripped result to stdout' \ +'lsp:Run the LSP 3.18 server over stdio' \ +'init:Write a starter .ocomment.toml or Lefthook configuration' \ +'config:Show, locate, explain, or export the resolved configuration' \ +'languages:List built-in languages, extensions, and dialects' \ +'plugin:Manage sandboxed WASM scanner plugins' \ +'completions:Generate shell completions' \ +'doctor:Diagnose the environment (config, git, plugins, tools)' \ +'man:Render the roff manual page to stdout' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'ocomment commands' commands "$@" @@ -625,18 +1809,19 @@ _ocomment__subcmd__fix_commands() { (( $+functions[_ocomment__subcmd__help_commands] )) || _ocomment__subcmd__help_commands() { local commands; commands=( -'check:' \ -'fix:' \ -'diff:' \ -'scan:' \ -'strip:' \ -'lsp:' \ -'init:' \ -'config:' \ -'languages:' \ -'plugin:' \ -'completions:' \ -'doctor:' \ +'check:Report removable comments (default command)' \ +'fix:Remove comments in place through an atomic, rollback-backed transaction' \ +'diff:Print a unified diff of the changes fix would make' \ +'scan:List every comment with its kind, disposition and byte span' \ +'strip:Read source on stdin and write the stripped result to stdout' \ +'lsp:Run the LSP 3.18 server over stdio' \ +'init:Write a starter .ocomment.toml or Lefthook configuration' \ +'config:Show, locate, explain, or export the resolved configuration' \ +'languages:List built-in languages, extensions, and dialects' \ +'plugin:Manage sandboxed WASM scanner plugins' \ +'completions:Generate shell completions' \ +'doctor:Diagnose the environment (config, git, plugins, tools)' \ +'man:Render the roff manual page to stdout' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'ocomment help commands' commands "$@" @@ -691,15 +1876,20 @@ _ocomment__subcmd__help__subcmd__lsp_commands() { local commands; commands=() _describe -t commands 'ocomment help lsp commands' commands "$@" } +(( $+functions[_ocomment__subcmd__help__subcmd__man_commands] )) || +_ocomment__subcmd__help__subcmd__man_commands() { + local commands; commands=() + _describe -t commands 'ocomment help man commands' commands "$@" +} (( $+functions[_ocomment__subcmd__help__subcmd__plugin_commands] )) || _ocomment__subcmd__help__subcmd__plugin_commands() { local commands; commands=( -'add:' \ -'remove:' \ -'list:' \ -'update:' \ -'verify:' \ -'new:' \ +'add:Install a plugin and pin its digest in .ocomment.lock' \ +'remove:Uninstall a plugin and drop its lock entry' \ +'list:List the installed plugins and their pinned digests' \ +'update:Re-fetch plugins and refresh their pinned digests' \ +'verify:Check installed plugins against their pinned digests' \ +'new:Scaffold a new plugin crate from the scanner WIT world' \ ) _describe -t commands 'ocomment help plugin commands' commands "$@" } @@ -758,15 +1948,20 @@ _ocomment__subcmd__lsp_commands() { local commands; commands=() _describe -t commands 'ocomment lsp commands' commands "$@" } +(( $+functions[_ocomment__subcmd__man_commands] )) || +_ocomment__subcmd__man_commands() { + local commands; commands=() + _describe -t commands 'ocomment man commands' commands "$@" +} (( $+functions[_ocomment__subcmd__plugin_commands] )) || _ocomment__subcmd__plugin_commands() { local commands; commands=( -'add:' \ -'remove:' \ -'list:' \ -'update:' \ -'verify:' \ -'new:' \ +'add:Install a plugin and pin its digest in .ocomment.lock' \ +'remove:Uninstall a plugin and drop its lock entry' \ +'list:List the installed plugins and their pinned digests' \ +'update:Re-fetch plugins and refresh their pinned digests' \ +'verify:Check installed plugins against their pinned digests' \ +'new:Scaffold a new plugin crate from the scanner WIT world' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'ocomment plugin commands' commands "$@" @@ -779,12 +1974,12 @@ _ocomment__subcmd__plugin__subcmd__add_commands() { (( $+functions[_ocomment__subcmd__plugin__subcmd__help_commands] )) || _ocomment__subcmd__plugin__subcmd__help_commands() { local commands; commands=( -'add:' \ -'remove:' \ -'list:' \ -'update:' \ -'verify:' \ -'new:' \ +'add:Install a plugin and pin its digest in .ocomment.lock' \ +'remove:Uninstall a plugin and drop its lock entry' \ +'list:List the installed plugins and their pinned digests' \ +'update:Re-fetch plugins and refresh their pinned digests' \ +'verify:Check installed plugins against their pinned digests' \ +'new:Scaffold a new plugin crate from the scanner WIT world' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'ocomment plugin help commands' commands "$@" diff --git a/release-extras/_ocomment.ps1 b/release-extras/_ocomment.ps1 index 3cd89eb..ccd9dcb 100644 --- a/release-extras/_ocomment.ps1 +++ b/release-extras/_ocomment.ps1 @@ -21,352 +21,438 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { $completions = @(switch ($command) { 'ocomment' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') - [CompletionResult]::new('check', 'check', [CompletionResultType]::ParameterValue, 'check') - [CompletionResult]::new('fix', 'fix', [CompletionResultType]::ParameterValue, 'fix') - [CompletionResult]::new('diff', 'diff', [CompletionResultType]::ParameterValue, 'diff') - [CompletionResult]::new('scan', 'scan', [CompletionResultType]::ParameterValue, 'scan') - [CompletionResult]::new('strip', 'strip', [CompletionResultType]::ParameterValue, 'strip') - [CompletionResult]::new('lsp', 'lsp', [CompletionResultType]::ParameterValue, 'lsp') - [CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'init') - [CompletionResult]::new('config', 'config', [CompletionResultType]::ParameterValue, 'config') - [CompletionResult]::new('languages', 'languages', [CompletionResultType]::ParameterValue, 'languages') - [CompletionResult]::new('plugin', 'plugin', [CompletionResultType]::ParameterValue, 'plugin') - [CompletionResult]::new('completions', 'completions', [CompletionResultType]::ParameterValue, 'completions') - [CompletionResult]::new('doctor', 'doctor', [CompletionResultType]::ParameterValue, 'doctor') + [CompletionResult]::new('check', 'check', [CompletionResultType]::ParameterValue, 'Report removable comments (default command)') + [CompletionResult]::new('fix', 'fix', [CompletionResultType]::ParameterValue, 'Remove comments in place through an atomic, rollback-backed transaction') + [CompletionResult]::new('diff', 'diff', [CompletionResultType]::ParameterValue, 'Print a unified diff of the changes fix would make') + [CompletionResult]::new('scan', 'scan', [CompletionResultType]::ParameterValue, 'List every comment with its kind, disposition and byte span') + [CompletionResult]::new('strip', 'strip', [CompletionResultType]::ParameterValue, 'Read source on stdin and write the stripped result to stdout') + [CompletionResult]::new('lsp', 'lsp', [CompletionResultType]::ParameterValue, 'Run the LSP 3.18 server over stdio') + [CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Write a starter .ocomment.toml or Lefthook configuration') + [CompletionResult]::new('config', 'config', [CompletionResultType]::ParameterValue, 'Show, locate, explain, or export the resolved configuration') + [CompletionResult]::new('languages', 'languages', [CompletionResultType]::ParameterValue, 'List built-in languages, extensions, and dialects') + [CompletionResult]::new('plugin', 'plugin', [CompletionResultType]::ParameterValue, 'Manage sandboxed WASM scanner plugins') + [CompletionResult]::new('completions', 'completions', [CompletionResultType]::ParameterValue, 'Generate shell completions') + [CompletionResult]::new('doctor', 'doctor', [CompletionResultType]::ParameterValue, 'Diagnose the environment (config, git, plugins, tools)') + [CompletionResult]::new('man', 'man', [CompletionResultType]::ParameterValue, 'Render the roff manual page to stdout') [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break } 'ocomment;check' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') [CompletionResult]::new('--staged', '--staged', [CompletionResultType]::ParameterName, 'Read and update Git index blobs rather than treating the working tree as the source') [CompletionResult]::new('--index-only', '--index-only', [CompletionResultType]::ParameterName, 'With `--staged`, do not attempt a uniquely mappable working-tree update') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;fix' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') [CompletionResult]::new('--staged', '--staged', [CompletionResultType]::ParameterName, 'Read and update Git index blobs rather than treating the working tree as the source') [CompletionResult]::new('--index-only', '--index-only', [CompletionResultType]::ParameterName, 'With `--staged`, do not attempt a uniquely mappable working-tree update') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;diff' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') [CompletionResult]::new('--staged', '--staged', [CompletionResultType]::ParameterName, 'Read and update Git index blobs rather than treating the working tree as the source') [CompletionResult]::new('--index-only', '--index-only', [CompletionResultType]::ParameterName, 'With `--staged`, do not attempt a uniquely mappable working-tree update') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;scan' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') [CompletionResult]::new('--staged', '--staged', [CompletionResultType]::ParameterName, 'Read and update Git index blobs rather than treating the working tree as the source') [CompletionResult]::new('--index-only', '--index-only', [CompletionResultType]::ParameterName, 'With `--staged`, do not attempt a uniquely mappable working-tree update') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;strip' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;lsp' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;init' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--fix', '--fix', [CompletionResultType]::ParameterName, 'fix') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--fix', '--fix', [CompletionResultType]::ParameterName, 'For the Lefthook hook, run `fix` instead of `check`') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;config' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;languages' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;plugin' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'add') - [CompletionResult]::new('remove', 'remove', [CompletionResultType]::ParameterValue, 'remove') - [CompletionResult]::new('list', 'list', [CompletionResultType]::ParameterValue, 'list') - [CompletionResult]::new('update', 'update', [CompletionResultType]::ParameterValue, 'update') - [CompletionResult]::new('verify', 'verify', [CompletionResultType]::ParameterValue, 'verify') - [CompletionResult]::new('new', 'new', [CompletionResultType]::ParameterValue, 'new') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Install a plugin and pin its digest in .ocomment.lock') + [CompletionResult]::new('remove', 'remove', [CompletionResultType]::ParameterValue, 'Uninstall a plugin and drop its lock entry') + [CompletionResult]::new('list', 'list', [CompletionResultType]::ParameterValue, 'List the installed plugins and their pinned digests') + [CompletionResult]::new('update', 'update', [CompletionResultType]::ParameterValue, 'Re-fetch plugins and refresh their pinned digests') + [CompletionResult]::new('verify', 'verify', [CompletionResultType]::ParameterValue, 'Check installed plugins against their pinned digests') + [CompletionResult]::new('new', 'new', [CompletionResultType]::ParameterValue, 'Scaffold a new plugin crate from the scanner WIT world') [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break } 'ocomment;plugin;add' { - [CompletionResult]::new('--name', '--name', [CompletionResultType]::ParameterName, 'name') - [CompletionResult]::new('--sha256', '--sha256', [CompletionResultType]::ParameterName, 'sha256') - [CompletionResult]::new('--identity', '--identity', [CompletionResultType]::ParameterName, 'identity') - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--name', '--name', [CompletionResultType]::ParameterName, 'Name to register the plugin under (default: the file stem)') + [CompletionResult]::new('--sha256', '--sha256', [CompletionResultType]::ParameterName, 'Expected SHA-256 digest of the component, verified before install') + [CompletionResult]::new('--identity', '--identity', [CompletionResultType]::ParameterName, 'Publisher identity recorded alongside the pinned digest') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;plugin;remove' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;plugin;list' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;plugin;update' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;plugin;verify' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;plugin;new' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;plugin;help' { - [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'add') - [CompletionResult]::new('remove', 'remove', [CompletionResultType]::ParameterValue, 'remove') - [CompletionResult]::new('list', 'list', [CompletionResultType]::ParameterValue, 'list') - [CompletionResult]::new('update', 'update', [CompletionResultType]::ParameterValue, 'update') - [CompletionResult]::new('verify', 'verify', [CompletionResultType]::ParameterValue, 'verify') - [CompletionResult]::new('new', 'new', [CompletionResultType]::ParameterValue, 'new') + [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Install a plugin and pin its digest in .ocomment.lock') + [CompletionResult]::new('remove', 'remove', [CompletionResultType]::ParameterValue, 'Uninstall a plugin and drop its lock entry') + [CompletionResult]::new('list', 'list', [CompletionResultType]::ParameterValue, 'List the installed plugins and their pinned digests') + [CompletionResult]::new('update', 'update', [CompletionResultType]::ParameterValue, 'Re-fetch plugins and refresh their pinned digests') + [CompletionResult]::new('verify', 'verify', [CompletionResultType]::ParameterValue, 'Check installed plugins against their pinned digests') + [CompletionResult]::new('new', 'new', [CompletionResultType]::ParameterValue, 'Scaffold a new plugin crate from the scanner WIT world') [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break } @@ -392,54 +478,88 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { break } 'ocomment;completions' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;doctor' { - [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Explicit configuration file') + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') - [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'policy') - [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'layout') - [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'language') - [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'dialect') - [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'keep-kind') - [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'remove-kind') - [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'color') - [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'hyperlinks') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'progress') - [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'force-invalid') - [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'force-protected') - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + break + } + 'ocomment;man' { + [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Read this configuration file instead of discovering `.ocomment.toml`') + [CompletionResult]::new('--policy', '--policy', [CompletionResultType]::ParameterName, 'Which classes of comment the run is allowed to remove') + [CompletionResult]::new('--layout', '--layout', [CompletionResultType]::ParameterName, 'How the bytes left behind by a removed comment are laid out') + [CompletionResult]::new('--language', '--language', [CompletionResultType]::ParameterName, 'Force this language instead of detecting it from path and contents') + [CompletionResult]::new('--dialect', '--dialect', [CompletionResultType]::ParameterName, 'Force this dialect of the selected language') + [CompletionResult]::new('--keep-kind', '--keep-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to protect on top of the policy') + [CompletionResult]::new('--remove-kind', '--remove-kind', [CompletionResultType]::ParameterName, 'Comma-separated comment kinds to remove regardless of the policy') + [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') + [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') + [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') + [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') + [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') + [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') + [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') break } 'ocomment;help' { - [CompletionResult]::new('check', 'check', [CompletionResultType]::ParameterValue, 'check') - [CompletionResult]::new('fix', 'fix', [CompletionResultType]::ParameterValue, 'fix') - [CompletionResult]::new('diff', 'diff', [CompletionResultType]::ParameterValue, 'diff') - [CompletionResult]::new('scan', 'scan', [CompletionResultType]::ParameterValue, 'scan') - [CompletionResult]::new('strip', 'strip', [CompletionResultType]::ParameterValue, 'strip') - [CompletionResult]::new('lsp', 'lsp', [CompletionResultType]::ParameterValue, 'lsp') - [CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'init') - [CompletionResult]::new('config', 'config', [CompletionResultType]::ParameterValue, 'config') - [CompletionResult]::new('languages', 'languages', [CompletionResultType]::ParameterValue, 'languages') - [CompletionResult]::new('plugin', 'plugin', [CompletionResultType]::ParameterValue, 'plugin') - [CompletionResult]::new('completions', 'completions', [CompletionResultType]::ParameterValue, 'completions') - [CompletionResult]::new('doctor', 'doctor', [CompletionResultType]::ParameterValue, 'doctor') + [CompletionResult]::new('check', 'check', [CompletionResultType]::ParameterValue, 'Report removable comments (default command)') + [CompletionResult]::new('fix', 'fix', [CompletionResultType]::ParameterValue, 'Remove comments in place through an atomic, rollback-backed transaction') + [CompletionResult]::new('diff', 'diff', [CompletionResultType]::ParameterValue, 'Print a unified diff of the changes fix would make') + [CompletionResult]::new('scan', 'scan', [CompletionResultType]::ParameterValue, 'List every comment with its kind, disposition and byte span') + [CompletionResult]::new('strip', 'strip', [CompletionResultType]::ParameterValue, 'Read source on stdin and write the stripped result to stdout') + [CompletionResult]::new('lsp', 'lsp', [CompletionResultType]::ParameterValue, 'Run the LSP 3.18 server over stdio') + [CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Write a starter .ocomment.toml or Lefthook configuration') + [CompletionResult]::new('config', 'config', [CompletionResultType]::ParameterValue, 'Show, locate, explain, or export the resolved configuration') + [CompletionResult]::new('languages', 'languages', [CompletionResultType]::ParameterValue, 'List built-in languages, extensions, and dialects') + [CompletionResult]::new('plugin', 'plugin', [CompletionResultType]::ParameterValue, 'Manage sandboxed WASM scanner plugins') + [CompletionResult]::new('completions', 'completions', [CompletionResultType]::ParameterValue, 'Generate shell completions') + [CompletionResult]::new('doctor', 'doctor', [CompletionResultType]::ParameterValue, 'Diagnose the environment (config, git, plugins, tools)') + [CompletionResult]::new('man', 'man', [CompletionResultType]::ParameterValue, 'Render the roff manual page to stdout') [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break } @@ -471,12 +591,12 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { break } 'ocomment;help;plugin' { - [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'add') - [CompletionResult]::new('remove', 'remove', [CompletionResultType]::ParameterValue, 'remove') - [CompletionResult]::new('list', 'list', [CompletionResultType]::ParameterValue, 'list') - [CompletionResult]::new('update', 'update', [CompletionResultType]::ParameterValue, 'update') - [CompletionResult]::new('verify', 'verify', [CompletionResultType]::ParameterValue, 'verify') - [CompletionResult]::new('new', 'new', [CompletionResultType]::ParameterValue, 'new') + [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Install a plugin and pin its digest in .ocomment.lock') + [CompletionResult]::new('remove', 'remove', [CompletionResultType]::ParameterValue, 'Uninstall a plugin and drop its lock entry') + [CompletionResult]::new('list', 'list', [CompletionResultType]::ParameterValue, 'List the installed plugins and their pinned digests') + [CompletionResult]::new('update', 'update', [CompletionResultType]::ParameterValue, 'Re-fetch plugins and refresh their pinned digests') + [CompletionResult]::new('verify', 'verify', [CompletionResultType]::ParameterValue, 'Check installed plugins against their pinned digests') + [CompletionResult]::new('new', 'new', [CompletionResultType]::ParameterValue, 'Scaffold a new plugin crate from the scanner WIT world') break } 'ocomment;help;plugin;add' { @@ -503,6 +623,9 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { 'ocomment;help;doctor' { break } + 'ocomment;help;man' { + break + } 'ocomment;help;help' { break } diff --git a/release-extras/ocomment.1 b/release-extras/ocomment.1 index cffafdb..9ec68fd 100644 --- a/release-extras/ocomment.1 +++ b/release-extras/ocomment.1 @@ -1,86 +1,343 @@ -.TH OCOMMENT 1 "August 2026" "ocomment 0.1.0" "User Commands" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.TH OCOMMENT 1 "ocomment 0.1.0" "User Commands" .SH NAME -ocomment \- check and remove source-code comments safely +ocomment \- Check and remove source\-code comments safely .SH SYNOPSIS -.B ocomment -.RI [ PATH ...] -.br -.B ocomment -.RI COMMAND " [OPTIONS]" +\fBocomment\fR [\fB\-\-config\fR] [\fB\-\-policy\fR] [\fB\-\-layout\fR] [\fB\-\-language\fR] [\fB\-\-dialect\fR] [\fB\-\-keep\-kind\fR] [\fB\-\-remove\-kind\fR] [\fB\-\-force\-invalid\fR] [\fB\-\-force\-protected\fR] [\fB\-\-format\fR] [\fB\-\-color\fR] [\fB\-\-hyperlinks\fR] [\fB\-\-no\-preview\fR] [\fB\-\-progress\fR] [\fB\-q\fR|\fB\-\-quiet\fR] [\fB\-v\fR|\fB\-\-verbose\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIPATH\fR] [\fIsubcommands\fR] .SH DESCRIPTION -OComment scans source bytes without requiring UTF-8 and reports or removes -comment tokens. The default policy protects source preambles and tool or -language directives. Rewrites are prepared and committed as one rollback-backed -transaction. -.SH COMMANDS +OComment scans source bytes without requiring UTF\-8 and reports or removes comment tokens. The default policy protects source preambles and tool or language directives. Rewrites are prepared and committed as one rollback\-backed transaction. +.SH OPTIONS .TP -.B check [PATH...] -Report removable comments. This is the default command. +\fB\-\-config\fR \fI\fR +Read this configuration file instead of discovering `.ocomment.toml` .TP -.B fix [PATH...] -Atomically apply safe edits. +\fB\-h\fR, \fB\-\-help\fR +Print help (see a summary with \*(Aq\-h\*(Aq) .TP -.B diff [PATH...] -Print unified differences. +\fB\-V\fR, \fB\-\-version\fR +Print version .TP -.B strip -Read source from standard input and write transformed bytes to standard output. +[\fIPATH\fR] +Files or directories to check (default: current directory) +.SH POLICY .TP -.B scan [PATH...] -Print comment spans, kinds, dispositions, diagnostics, edits, and source maps. +\fB\-\-policy\fR \fI\fR +Which classes of comment the run is allowed to remove +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +safe: Remove ordinary and doc comments; keep preambles and directives +.IP \(bu 2 +legal: Like safe, and keep licence and copyright comments as well +.IP \(bu 2 +all: Remove every comment that no keep override protects +.RE .TP -.B lsp -Run the LSP 3.18 server over stdio. +\fB\-\-layout\fR \fI\fR +How the bytes left behind by a removed comment are laid out +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +lines: Keep the line structure and separate tokens that would otherwise join +.IP \(bu 2 +columns: Pad each removed comment so the following columns do not shift +.IP \(bu 2 +compact: Drop the comment bytes and keep only the newlines it spanned +.RE .TP -.B init, config, languages, plugin, completions, doctor -Manage configuration, integrations, plugins, generated completions, and -environment diagnostics. -.SH OPTIONS +\fB\-\-language\fR \fI\fR +Force this language instead of detecting it from path and contents +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +rust: Rust source files +.IP \(bu 2 +ocaml: OCaml implementation and interface files +.IP \(bu 2 +c: C source and header files +.IP \(bu 2 +cpp: C++ source and header files +.IP \(bu 2 +go: Go source files +.IP \(bu 2 +java: Java source files, including Unicode escape translation +.IP \(bu 2 +javascript: JavaScript modules and scripts, including JSX +.IP \(bu 2 +typescript: TypeScript modules and scripts, including TSX +.IP \(bu 2 +python: Python source and stub files +.IP \(bu 2 +shell: POSIX sh, Bash, and zsh scripts +.IP \(bu 2 +html: HTML documents, including nested script and style elements +.IP \(bu 2 +css: CSS stylesheets +.IP \(bu 2 +jsonc: JSON with comments, including JSON5 +.IP \(bu 2 +sql: SQL for every supported database dialect +.IP \(bu 2 +kotlin: Kotlin source and script files +.RE +.TP +\fB\-\-dialect\fR \fI\fR +Force this dialect of the selected language +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +standard: The default lexical rules of the language +.IP \(bu 2 +jsx: JavaScript with JSX elements +.IP \(bu 2 +tsx: TypeScript with JSX elements +.IP \(bu 2 +objective\-c: Objective\-C extensions to C +.IP \(bu 2 +objective\-cpp: Objective\-C++ extensions to C++ +.IP \(bu 2 +gnu\-c: GNU extensions to C +.IP \(bu 2 +gnu\-cpp: GNU extensions to C++ +.IP \(bu 2 +cuda: CUDA extensions to C++ +.IP \(bu 2 +posix\-sh: The POSIX shell command language +.IP \(bu 2 +bash53: Bash 5.3 +.IP \(bu 2 +zsh: The Z shell +.IP \(bu 2 +postgresql: PostgreSQL, with dollar\-quoted bodies +.IP \(bu 2 +mysql: MySQL, including its executable versioned comments +.IP \(bu 2 +sqlite: SQLite +.IP \(bu 2 +t\-sql: Microsoft Transact\-SQL +.IP \(bu 2 +oracle: Oracle SQL and PL/SQL +.RE +.TP +\fB\-\-keep\-kind\fR \fI\fR +Comma\-separated comment kinds to protect on top of the policy +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +line: An ordinary comment running to the end of the line +.IP \(bu 2 +block: An ordinary delimited comment +.IP \(bu 2 +doc\-line: A documentation comment running to the end of the line +.IP \(bu 2 +doc\-block: A delimited documentation comment +.IP \(bu 2 +directive: A tool or language directive such as a pragma or lint control +.IP \(bu 2 +license: A licence or copyright preamble +.IP \(bu 2 +html\-comment: A DOM\-observable HTML comment +.IP \(bu 2 +shebang: The interpreter line starting an executable script +.IP \(bu 2 +encoding: A source encoding declaration +.IP \(bu 2 +optimizer\-hint: A compiler or database optimizer hint +.IP \(bu 2 +version\-comment: A MySQL versioned comment that the server executes +.RE +.TP +\fB\-\-remove\-kind\fR \fI\fR +Comma\-separated comment kinds to remove regardless of the policy +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +line: An ordinary comment running to the end of the line +.IP \(bu 2 +block: An ordinary delimited comment +.IP \(bu 2 +doc\-line: A documentation comment running to the end of the line +.IP \(bu 2 +doc\-block: A delimited documentation comment +.IP \(bu 2 +directive: A tool or language directive such as a pragma or lint control +.IP \(bu 2 +license: A licence or copyright preamble +.IP \(bu 2 +html\-comment: A DOM\-observable HTML comment +.IP \(bu 2 +shebang: The interpreter line starting an executable script +.IP \(bu 2 +encoding: A source encoding declaration +.IP \(bu 2 +optimizer\-hint: A compiler or database optimizer hint +.IP \(bu 2 +version\-comment: A MySQL versioned comment that the server executes +.RE +.TP +\fB\-\-force\-invalid\fR +Apply the edits that are still provably safe when the source fails to scan +.TP +\fB\-\-force\-protected\fR +Remove protected comments such as shebang and encoding preambles +.SH OUTPUT +.TP +\fB\-\-format\fR \fI\fR [default: human] +Output encoding +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +human +.IP \(bu 2 +json +.IP \(bu 2 +jsonl +.IP \(bu 2 +sarif +.IP \(bu 2 +github +.RE +.TP +\fB\-\-color\fR \fI\fR [default: auto] +When to colour terminal output +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +auto +.IP \(bu 2 +always +.IP \(bu 2 +never +.RE +.TP +\fB\-\-hyperlinks\fR \fI\fR [default: auto] +When to emit terminal hyperlinks for reported paths +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +auto +.IP \(bu 2 +always +.IP \(bu 2 +never +.RE +.TP +\fB\-\-no\-preview\fR +Omit the one\-line comment text from human `check` and `scan` lines +.TP +\fB\-\-progress\fR \fI\fR [default: auto] +Accepted for compatibility; the end\-of\-run summary replaced this line +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +auto +.IP \(bu 2 +always +.IP \(bu 2 +never +.RE +.TP +\fB\-q\fR, \fB\-\-quiet\fR +Print nothing but errors and diagnostics +.TP +\fB\-v\fR, \fB\-\-verbose\fR +Trace what is scanned and summarize every comment kind and skipped file +.SH SUBCOMMANDS .TP -.BI --format " human|json|jsonl|sarif|github" -Select output format. +ocomment\-check(1) +Report removable comments (default command) .TP -.BI --policy " safe|legal|all" -Select removal policy. +ocomment\-fix(1) +Remove comments in place through an atomic, rollback\-backed transaction .TP -.BI --layout " lines|columns|compact" -Select replacement layout. +ocomment\-diff(1) +Print a unified diff of the changes fix would make .TP -.BI --language " LANGUAGE" -Override language detection. +ocomment\-scan(1) +List every comment with its kind, disposition and byte span .TP -.BI --dialect " DIALECT" -Select a guaranteed language dialect. +ocomment\-strip(1) +Read source on stdin and write the stripped result to stdout .TP -.BI --keep-kind " KIND" -Keep a comment kind. May be repeated or comma separated. +ocomment\-lsp(1) +Run the LSP 3.18 server over stdio .TP -.BI --remove-kind " KIND" -Remove a comment kind. May be repeated or comma separated. +ocomment\-init(1) +Write a starter .ocomment.toml or Lefthook configuration .TP -.B --force-invalid -Apply known safe spans even when the lexical scan reports invalid input. +ocomment\-config(1) +Show, locate, explain, or export the resolved configuration .TP -.B --force-protected -Allow the all policy to remove protected comment preambles. +ocomment\-languages(1) +List built\-in languages, extensions, and dialects .TP -.B --staged -Operate on comments whose starting delimiter is in a Git index addition. +ocomment\-plugin(1) +Manage sandboxed WASM scanner plugins .TP -.B --index-only -With --staged, update only the Git index. -.SH EXIT STATUS -0 means clean or a successful transformation. 1 means check or diff found a -change. 2 means configuration, I/O, source, staged-mapping, or plugin failure. -.SH FILES +ocomment\-completions(1) +Generate shell completions .TP -.B .ocomment.toml -Project configuration, version 1. +ocomment\-doctor(1) +Diagnose the environment (config, git, plugins, tools) .TP -.B .ocommentignore -Additional ignore patterns. +ocomment\-man(1) +Render the roff manual page to stdout .TP -.B .ocomment.lock -Pinned plugin artifacts and identities. -.SH SEE ALSO -The complete schemas and guides are available in the OComment repository. +ocomment\-help(1) +Print this message or the help of the given subcommand(s) +.SH EXTRA +EXIT STATUS + 0 Nothing removable was found and every requested change was applied. + 1 Removable comments were reported, or a diff was printed. + 2 Invalid source, configuration, plugin, or I/O failure. + +FILES + .ocomment.toml Project configuration, merged over the user file. + .ocommentignore Extra ignore patterns honoured by repository walks. + .ocomment.lock Pinned digests of the installed WASM scanner plugins. + $XDG_CONFIG_HOME/ocomment/config.toml + User configuration, merged over the built\-in defaults. + +EXAMPLES + ocomment + Check the current repository and report removable comments. + ocomment fix \-\-policy all \-\-layout compact src + Remove every comment under src and close the gaps it leaves. + ocomment strip \-\-language rust < before.rs > after.rs + Strip one file from standard input to standard output. + +SEE ALSO + The complete schemas and guides are available in the OComment repository. +.SH VERSION +v0.1.0 diff --git a/release-extras/ocomment.bash b/release-extras/ocomment.bash index 34e6456..3252c8e 100644 --- a/release-extras/ocomment.bash +++ b/release-extras/ocomment.bash @@ -46,6 +46,9 @@ _ocomment() { ocomment,lsp) cmd="ocomment__subcmd__lsp" ;; + ocomment,man) + cmd="ocomment__subcmd__man" + ;; ocomment,plugin) cmd="ocomment__subcmd__plugin" ;; @@ -85,6 +88,9 @@ _ocomment() { ocomment__subcmd__help,lsp) cmd="ocomment__subcmd__help__subcmd__lsp" ;; + ocomment__subcmd__help,man) + cmd="ocomment__subcmd__help__subcmd__man" + ;; ocomment__subcmd__help,plugin) cmd="ocomment__subcmd__help__subcmd__plugin" ;; @@ -161,7 +167,7 @@ _ocomment() { case "${cmd}" in ocomment) - opts="-h -V --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help --version check fix diff scan strip lsp init config languages plugin completions doctor help" + opts="-q -v -h -V --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help --version check fix diff scan strip lsp init config languages plugin completions doctor man help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -171,32 +177,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -219,7 +225,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__check) - opts="-h --staged --index-only --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help" + opts="-q -v -h --staged --index-only --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -229,32 +235,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -277,7 +283,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__completions) - opts="-h --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help bash elvish fish powershell zsh" + opts="-q -v -h --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help bash elvish fish powershell zsh" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -287,32 +293,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -335,7 +341,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__config) - opts="-h --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help show locate explain schema" + opts="-q -v -h --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help show locate explain schema" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -345,32 +351,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -393,7 +399,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__diff) - opts="-h --staged --index-only --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help" + opts="-q -v -h --staged --index-only --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -403,32 +409,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -451,7 +457,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__doctor) - opts="-h --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help" + opts="-q -v -h --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -461,32 +467,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -509,7 +515,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__fix) - opts="-h --staged --index-only --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help" + opts="-q -v -h --staged --index-only --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -519,32 +525,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -567,7 +573,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__help) - opts="check fix diff scan strip lsp init config languages plugin completions doctor help" + opts="check fix diff scan strip lsp init config languages plugin completions doctor man help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -720,6 +726,20 @@ _ocomment() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + ocomment__subcmd__help__subcmd__man) + opts="" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; ocomment__subcmd__help__subcmd__plugin) opts="add remove list update verify new" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -847,7 +867,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__init) - opts="-h --fix --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help config lefthook" + opts="-q -v -h --fix --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help config lefthook" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -857,32 +877,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -905,7 +925,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__languages) - opts="-h --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help" + opts="-q -v -h --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -915,32 +935,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -963,7 +983,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__lsp) - opts="-h --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help" + opts="-q -v -h --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -973,32 +993,90 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --policy) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) + return 0 + ;; + --layout) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) + return 0 + ;; + --language) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) + return 0 + ;; + --dialect) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) + return 0 + ;; + --keep-kind) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --remove-kind) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; --format) COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; - --policy) + --color) + COMPREPLY=($(compgen -W "auto always never" -- "${cur}")) + return 0 + ;; + --hyperlinks) + COMPREPLY=($(compgen -W "auto always never" -- "${cur}")) + return 0 + ;; + --progress) + COMPREPLY=($(compgen -W "auto always never" -- "${cur}")) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + ocomment__subcmd__man) + opts="-q -v -h --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + --config) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --policy) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) + return 0 + ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -1021,7 +1099,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__plugin) - opts="-h --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help add remove list update verify new help" + opts="-q -v -h --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help add remove list update verify new help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1031,32 +1109,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -1079,7 +1157,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__plugin__subcmd__add) - opts="-h --name --sha256 --identity --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help" + opts="-q -v -h --name --sha256 --identity --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1101,32 +1179,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -1261,7 +1339,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__plugin__subcmd__list) - opts="-h --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help" + opts="-q -v -h --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1271,32 +1349,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -1319,7 +1397,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__plugin__subcmd__new) - opts="-h --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help" + opts="-q -v -h --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1329,32 +1407,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -1377,7 +1455,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__plugin__subcmd__remove) - opts="-h --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help" + opts="-q -v -h --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1387,32 +1465,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -1435,7 +1513,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__plugin__subcmd__update) - opts="-h --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help" + opts="-q -v -h --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1445,32 +1523,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -1493,7 +1571,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__plugin__subcmd__verify) - opts="-h --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help" + opts="-q -v -h --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1503,32 +1581,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -1551,7 +1629,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__scan) - opts="-h --staged --index-only --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help" + opts="-q -v -h --staged --index-only --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1561,32 +1639,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) @@ -1609,7 +1687,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__strip) - opts="-h --config --format --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --color --hyperlinks --progress --help" + opts="-q -v -h --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1619,32 +1697,32 @@ _ocomment() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --format) - COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) - return 0 - ;; --policy) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "safe legal all" -- "${cur}")) return 0 ;; --layout) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "lines columns compact" -- "${cur}")) return 0 ;; --language) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin" -- "${cur}")) return 0 ;; --dialect) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "standard jsx tsx objective-c objective-cpp gnu-c gnu-cpp cuda posix-sh bash53 zsh postgresql mysql sqlite t-sql oracle" -- "${cur}")) return 0 ;; --keep-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) return 0 ;; --remove-kind) - COMPREPLY=($(compgen -f "${cur}")) + COMPREPLY=($(compgen -W "line block doc-line doc-block directive license html-comment shebang encoding optimizer-hint version-comment" -- "${cur}")) + return 0 + ;; + --format) + COMPREPLY=($(compgen -W "human json jsonl sarif github" -- "${cur}")) return 0 ;; --color) diff --git a/release-extras/ocomment.elv b/release-extras/ocomment.elv index f2d2546..5c6a641 100644 --- a/release-extras/ocomment.elv +++ b/release-extras/ocomment.elv @@ -18,335 +18,421 @@ set edit:completion:arg-completer[ocomment] = {|@words| } var completions = [ &'ocomment'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' cand -V 'Print version' cand --version 'Print version' - cand check 'check' - cand fix 'fix' - cand diff 'diff' - cand scan 'scan' - cand strip 'strip' - cand lsp 'lsp' - cand init 'init' - cand config 'config' - cand languages 'languages' - cand plugin 'plugin' - cand completions 'completions' - cand doctor 'doctor' + cand check 'Report removable comments (default command)' + cand fix 'Remove comments in place through an atomic, rollback-backed transaction' + cand diff 'Print a unified diff of the changes fix would make' + cand scan 'List every comment with its kind, disposition and byte span' + cand strip 'Read source on stdin and write the stripped result to stdout' + cand lsp 'Run the LSP 3.18 server over stdio' + cand init 'Write a starter .ocomment.toml or Lefthook configuration' + cand config 'Show, locate, explain, or export the resolved configuration' + cand languages 'List built-in languages, extensions, and dialects' + cand plugin 'Manage sandboxed WASM scanner plugins' + cand completions 'Generate shell completions' + cand doctor 'Diagnose the environment (config, git, plugins, tools)' + cand man 'Render the roff manual page to stdout' cand help 'Print this message or the help of the given subcommand(s)' } &'ocomment;check'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' cand --staged 'Read and update Git index blobs rather than treating the working tree as the source' cand --index-only 'With `--staged`, do not attempt a uniquely mappable working-tree update' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;fix'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' cand --staged 'Read and update Git index blobs rather than treating the working tree as the source' cand --index-only 'With `--staged`, do not attempt a uniquely mappable working-tree update' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;diff'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' cand --staged 'Read and update Git index blobs rather than treating the working tree as the source' cand --index-only 'With `--staged`, do not attempt a uniquely mappable working-tree update' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;scan'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' cand --staged 'Read and update Git index blobs rather than treating the working tree as the source' cand --index-only 'With `--staged`, do not attempt a uniquely mappable working-tree update' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;strip'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;lsp'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;init'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --fix 'fix' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --fix 'For the Lefthook hook, run `fix` instead of `check`' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;config'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;languages'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;plugin'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' - cand add 'add' - cand remove 'remove' - cand list 'list' - cand update 'update' - cand verify 'verify' - cand new 'new' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' + cand add 'Install a plugin and pin its digest in .ocomment.lock' + cand remove 'Uninstall a plugin and drop its lock entry' + cand list 'List the installed plugins and their pinned digests' + cand update 'Re-fetch plugins and refresh their pinned digests' + cand verify 'Check installed plugins against their pinned digests' + cand new 'Scaffold a new plugin crate from the scanner WIT world' cand help 'Print this message or the help of the given subcommand(s)' } &'ocomment;plugin;add'= { - cand --name 'name' - cand --sha256 'sha256' - cand --identity 'identity' - cand --config 'Explicit configuration file' + cand --name 'Name to register the plugin under (default: the file stem)' + cand --sha256 'Expected SHA-256 digest of the component, verified before install' + cand --identity 'Publisher identity recorded alongside the pinned digest' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;plugin;remove'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;plugin;list'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;plugin;update'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;plugin;verify'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;plugin;new'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;plugin;help'= { - cand add 'add' - cand remove 'remove' - cand list 'list' - cand update 'update' - cand verify 'verify' - cand new 'new' + cand add 'Install a plugin and pin its digest in .ocomment.lock' + cand remove 'Uninstall a plugin and drop its lock entry' + cand list 'List the installed plugins and their pinned digests' + cand update 'Re-fetch plugins and refresh their pinned digests' + cand verify 'Check installed plugins against their pinned digests' + cand new 'Scaffold a new plugin crate from the scanner WIT world' cand help 'Print this message or the help of the given subcommand(s)' } &'ocomment;plugin;help;add'= { @@ -364,52 +450,85 @@ set edit:completion:arg-completer[ocomment] = {|@words| &'ocomment;plugin;help;help'= { } &'ocomment;completions'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;doctor'= { - cand --config 'Explicit configuration file' + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' cand --format 'Output encoding' - cand --policy 'policy' - cand --layout 'layout' - cand --language 'language' - cand --dialect 'dialect' - cand --keep-kind 'keep-kind' - cand --remove-kind 'remove-kind' - cand --color 'color' - cand --hyperlinks 'hyperlinks' - cand --progress 'progress' - cand --force-invalid 'force-invalid' - cand --force-protected 'force-protected' - cand -h 'Print help' - cand --help 'Print help' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' + } + &'ocomment;man'= { + cand --config 'Read this configuration file instead of discovering `.ocomment.toml`' + cand --policy 'Which classes of comment the run is allowed to remove' + cand --layout 'How the bytes left behind by a removed comment are laid out' + cand --language 'Force this language instead of detecting it from path and contents' + cand --dialect 'Force this dialect of the selected language' + cand --keep-kind 'Comma-separated comment kinds to protect on top of the policy' + cand --remove-kind 'Comma-separated comment kinds to remove regardless of the policy' + cand --format 'Output encoding' + cand --color 'When to colour terminal output' + cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' + cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' + cand --force-protected 'Remove protected comments such as shebang and encoding preambles' + cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' + cand -q 'Print nothing but errors and diagnostics' + cand --quiet 'Print nothing but errors and diagnostics' + cand -v 'Trace what is scanned and summarize every comment kind and skipped file' + cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' + cand -h 'Print help (see more with ''--help'')' + cand --help 'Print help (see more with ''--help'')' } &'ocomment;help'= { - cand check 'check' - cand fix 'fix' - cand diff 'diff' - cand scan 'scan' - cand strip 'strip' - cand lsp 'lsp' - cand init 'init' - cand config 'config' - cand languages 'languages' - cand plugin 'plugin' - cand completions 'completions' - cand doctor 'doctor' + cand check 'Report removable comments (default command)' + cand fix 'Remove comments in place through an atomic, rollback-backed transaction' + cand diff 'Print a unified diff of the changes fix would make' + cand scan 'List every comment with its kind, disposition and byte span' + cand strip 'Read source on stdin and write the stripped result to stdout' + cand lsp 'Run the LSP 3.18 server over stdio' + cand init 'Write a starter .ocomment.toml or Lefthook configuration' + cand config 'Show, locate, explain, or export the resolved configuration' + cand languages 'List built-in languages, extensions, and dialects' + cand plugin 'Manage sandboxed WASM scanner plugins' + cand completions 'Generate shell completions' + cand doctor 'Diagnose the environment (config, git, plugins, tools)' + cand man 'Render the roff manual page to stdout' cand help 'Print this message or the help of the given subcommand(s)' } &'ocomment;help;check'= { @@ -431,12 +550,12 @@ set edit:completion:arg-completer[ocomment] = {|@words| &'ocomment;help;languages'= { } &'ocomment;help;plugin'= { - cand add 'add' - cand remove 'remove' - cand list 'list' - cand update 'update' - cand verify 'verify' - cand new 'new' + cand add 'Install a plugin and pin its digest in .ocomment.lock' + cand remove 'Uninstall a plugin and drop its lock entry' + cand list 'List the installed plugins and their pinned digests' + cand update 'Re-fetch plugins and refresh their pinned digests' + cand verify 'Check installed plugins against their pinned digests' + cand new 'Scaffold a new plugin crate from the scanner WIT world' } &'ocomment;help;plugin;add'= { } @@ -454,6 +573,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| } &'ocomment;help;doctor'= { } + &'ocomment;help;man'= { + } &'ocomment;help;help'= { } ] diff --git a/release-extras/ocomment.fish b/release-extras/ocomment.fish index c1ceb49..01c2091 100644 --- a/release-extras/ocomment.fish +++ b/release-extras/ocomment.fish @@ -1,6 +1,6 @@ # Print an optspec for argparse to handle cmd's options that are independent of any subcommand. function __fish_ocomment_global_optspecs - string join \n config= format= policy= layout= language= dialect= keep-kind= remove-kind= force-invalid force-protected color= hyperlinks= progress= h/help V/version + string join \n config= policy= layout= language= dialect= keep-kind= remove-kind= force-invalid force-protected format= color= hyperlinks= no-preview progress= q/quiet v/verbose h/help V/version end function __fish_ocomment_needs_command @@ -24,518 +24,1664 @@ function __fish_ocomment_using_subcommand contains -- $cmd[1] $argv end -complete -c ocomment -n "__fish_ocomment_needs_command" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_needs_command" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_needs_command" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_needs_command" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_needs_command" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_needs_command" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_needs_command" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_needs_command" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_needs_command" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_needs_command" -l policy -r -complete -c ocomment -n "__fish_ocomment_needs_command" -l layout -r -complete -c ocomment -n "__fish_ocomment_needs_command" -l language -r -complete -c ocomment -n "__fish_ocomment_needs_command" -l dialect -r -complete -c ocomment -n "__fish_ocomment_needs_command" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_needs_command" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_needs_command" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_needs_command" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_needs_command" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_needs_command" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_needs_command" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_needs_command" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_needs_command" -l force-invalid -complete -c ocomment -n "__fish_ocomment_needs_command" -l force-protected -complete -c ocomment -n "__fish_ocomment_needs_command" -s h -l help -d 'Print help' +complete -c ocomment -n "__fish_ocomment_needs_command" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_needs_command" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_needs_command" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_needs_command" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_needs_command" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_needs_command" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_needs_command" -s V -l version -d 'Print version' -complete -c ocomment -n "__fish_ocomment_needs_command" -a "check" -complete -c ocomment -n "__fish_ocomment_needs_command" -a "fix" -complete -c ocomment -n "__fish_ocomment_needs_command" -a "diff" -complete -c ocomment -n "__fish_ocomment_needs_command" -a "scan" -complete -c ocomment -n "__fish_ocomment_needs_command" -a "strip" -complete -c ocomment -n "__fish_ocomment_needs_command" -a "lsp" -complete -c ocomment -n "__fish_ocomment_needs_command" -a "init" -complete -c ocomment -n "__fish_ocomment_needs_command" -a "config" -complete -c ocomment -n "__fish_ocomment_needs_command" -a "languages" -complete -c ocomment -n "__fish_ocomment_needs_command" -a "plugin" -complete -c ocomment -n "__fish_ocomment_needs_command" -a "completions" -complete -c ocomment -n "__fish_ocomment_needs_command" -a "doctor" +complete -c ocomment -n "__fish_ocomment_needs_command" -a "check" -d 'Report removable comments (default command)' +complete -c ocomment -n "__fish_ocomment_needs_command" -a "fix" -d 'Remove comments in place through an atomic, rollback-backed transaction' +complete -c ocomment -n "__fish_ocomment_needs_command" -a "diff" -d 'Print a unified diff of the changes fix would make' +complete -c ocomment -n "__fish_ocomment_needs_command" -a "scan" -d 'List every comment with its kind, disposition and byte span' +complete -c ocomment -n "__fish_ocomment_needs_command" -a "strip" -d 'Read source on stdin and write the stripped result to stdout' +complete -c ocomment -n "__fish_ocomment_needs_command" -a "lsp" -d 'Run the LSP 3.18 server over stdio' +complete -c ocomment -n "__fish_ocomment_needs_command" -a "init" -d 'Write a starter .ocomment.toml or Lefthook configuration' +complete -c ocomment -n "__fish_ocomment_needs_command" -a "config" -d 'Show, locate, explain, or export the resolved configuration' +complete -c ocomment -n "__fish_ocomment_needs_command" -a "languages" -d 'List built-in languages, extensions, and dialects' +complete -c ocomment -n "__fish_ocomment_needs_command" -a "plugin" -d 'Manage sandboxed WASM scanner plugins' +complete -c ocomment -n "__fish_ocomment_needs_command" -a "completions" -d 'Generate shell completions' +complete -c ocomment -n "__fish_ocomment_needs_command" -a "doctor" -d 'Diagnose the environment (config, git, plugins, tools)' +complete -c ocomment -n "__fish_ocomment_needs_command" -a "man" -d 'Render the roff manual page to stdout' complete -c ocomment -n "__fish_ocomment_needs_command" -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l staged -d 'Read and update Git index blobs rather than treating the working tree as the source' complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l index-only -d 'With `--staged`, do not attempt a uniquely mappable working-tree update' -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l staged -d 'Read and update Git index blobs rather than treating the working tree as the source' complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l index-only -d 'With `--staged`, do not attempt a uniquely mappable working-tree update' -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l staged -d 'Read and update Git index blobs rather than treating the working tree as the source' complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l index-only -d 'With `--staged`, do not attempt a uniquely mappable working-tree update' -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l staged -d 'Read and update Git index blobs rather than treating the working tree as the source' complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l index-only -d 'With `--staged`, do not attempt a uniquely mappable working-tree update' -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l fix -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l fix -d 'For the Lefthook hook, run `fix` instead of `check`' +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -f -a "add" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -f -a "remove" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -f -a "list" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -f -a "update" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -f -a "verify" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -f -a "new" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -f -a "add" -d 'Install a plugin and pin its digest in .ocomment.lock' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -f -a "remove" -d 'Uninstall a plugin and drop its lock entry' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -f -a "list" -d 'List the installed plugins and their pinned digests' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -f -a "update" -d 'Re-fetch plugins and refresh their pinned digests' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -f -a "verify" -d 'Check installed plugins against their pinned digests' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -f -a "new" -d 'Scaffold a new plugin crate from the scanner WIT world' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l name -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l sha256 -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l identity -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l name -d 'Name to register the plugin under (default: the file stem)' -r +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l sha256 -d 'Expected SHA-256 digest of the component, verified before install' -r +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l identity -d 'Publisher identity recorded alongside the pinned digest' -r +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from help" -f -a "add" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from help" -f -a "remove" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from help" -f -a "list" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from help" -f -a "update" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from help" -f -a "verify" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from help" -f -a "new" +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from help" -f -a "add" -d 'Install a plugin and pin its digest in .ocomment.lock' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from help" -f -a "remove" -d 'Uninstall a plugin and drop its lock entry' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from help" -f -a "list" -d 'List the installed plugins and their pinned digests' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from help" -f -a "update" -d 'Re-fetch plugins and refresh their pinned digests' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from help" -f -a "verify" -d 'Check installed plugins against their pinned digests' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from help" -f -a "new" -d 'Scaffold a new plugin crate from the scanner WIT world' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l color -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l hyperlinks -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l progress -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l config -d 'Explicit configuration file' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l format -d 'Output encoding' -r -f -a "human\t'' json\t'' jsonl\t'' sarif\t'' github\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l policy -r -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l layout -r -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l language -r -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l dialect -r -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l keep-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l remove-kind -r -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l color -r -f -a "auto\t'' -always\t'' -never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l hyperlinks -r -f -a "auto\t'' -always\t'' -never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l progress -r -f -a "auto\t'' -always\t'' -never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l force-invalid -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l force-protected -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -s h -l help -d 'Print help' -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor help" -f -a "check" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor help" -f -a "fix" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor help" -f -a "diff" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor help" -f -a "scan" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor help" -f -a "strip" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor help" -f -a "lsp" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor help" -f -a "init" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor help" -f -a "config" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor help" -f -a "languages" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor help" -f -a "plugin" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor help" -f -a "completions" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor help" -f -a "doctor" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and __fish_seen_subcommand_from plugin" -f -a "add" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and __fish_seen_subcommand_from plugin" -f -a "remove" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and __fish_seen_subcommand_from plugin" -f -a "list" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and __fish_seen_subcommand_from plugin" -f -a "update" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and __fish_seen_subcommand_from plugin" -f -a "verify" -complete -c ocomment -n "__fish_ocomment_using_subcommand help; and __fish_seen_subcommand_from plugin" -f -a "new" +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' +always\t'' +never\t''" +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' +always\t'' +never\t''" +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +always\t'' +never\t''" +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l policy -d 'Which classes of comment the run is allowed to remove' -r -f -a "safe\t'Remove ordinary and doc comments; keep preambles and directives' +legal\t'Like safe, and keep licence and copyright comments as well' +all\t'Remove every comment that no keep override protects'" +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l layout -d 'How the bytes left behind by a removed comment are laid out' -r -f -a "lines\t'Keep the line structure and separate tokens that would otherwise join' +columns\t'Pad each removed comment so the following columns do not shift' +compact\t'Drop the comment bytes and keep only the newlines it spanned'" +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l language -d 'Force this language instead of detecting it from path and contents' -r -f -a "rust\t'Rust source files' +ocaml\t'OCaml implementation and interface files' +c\t'C source and header files' +cpp\t'C++ source and header files' +go\t'Go source files' +java\t'Java source files, including Unicode escape translation' +javascript\t'JavaScript modules and scripts, including JSX' +typescript\t'TypeScript modules and scripts, including TSX' +python\t'Python source and stub files' +shell\t'POSIX sh, Bash, and zsh scripts' +html\t'HTML documents, including nested script and style elements' +css\t'CSS stylesheets' +jsonc\t'JSON with comments, including JSON5' +sql\t'SQL for every supported database dialect' +kotlin\t'Kotlin source and script files'" +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' +jsx\t'JavaScript with JSX elements' +tsx\t'TypeScript with JSX elements' +objective-c\t'Objective-C extensions to C' +objective-cpp\t'Objective-C++ extensions to C++' +gnu-c\t'GNU extensions to C' +gnu-cpp\t'GNU extensions to C++' +cuda\t'CUDA extensions to C++' +posix-sh\t'The POSIX shell command language' +bash53\t'Bash 5.3' +zsh\t'The Z shell' +postgresql\t'PostgreSQL, with dollar-quoted bodies' +mysql\t'MySQL, including its executable versioned comments' +sqlite\t'SQLite' +t-sql\t'Microsoft Transact-SQL' +oracle\t'Oracle SQL and PL/SQL'" +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l keep-kind -d 'Comma-separated comment kinds to protect on top of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l remove-kind -d 'Comma-separated comment kinds to remove regardless of the policy' -r -f -a "line\t'An ordinary comment running to the end of the line' +block\t'An ordinary delimited comment' +doc-line\t'A documentation comment running to the end of the line' +doc-block\t'A delimited documentation comment' +directive\t'A tool or language directive such as a pragma or lint control' +license\t'A licence or copyright preamble' +html-comment\t'A DOM-observable HTML comment' +shebang\t'The interpreter line starting an executable script' +encoding\t'A source encoding declaration' +optimizer-hint\t'A compiler or database optimizer hint' +version-comment\t'A MySQL versioned comment that the server executes'" +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l format -d 'Output encoding' -r -f -a "human\t'' +json\t'' +jsonl\t'' +sarif\t'' +github\t''" +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l color -d 'When to colour terminal output' -r -f -a "auto\t'' +always\t'' +never\t''" +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' +always\t'' +never\t''" +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +always\t'' +never\t''" +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -s h -l help -d 'Print help (see more with \'--help\')' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "check" -d 'Report removable comments (default command)' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "fix" -d 'Remove comments in place through an atomic, rollback-backed transaction' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "diff" -d 'Print a unified diff of the changes fix would make' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "scan" -d 'List every comment with its kind, disposition and byte span' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "strip" -d 'Read source on stdin and write the stripped result to stdout' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "lsp" -d 'Run the LSP 3.18 server over stdio' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "init" -d 'Write a starter .ocomment.toml or Lefthook configuration' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "config" -d 'Show, locate, explain, or export the resolved configuration' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "languages" -d 'List built-in languages, extensions, and dialects' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "plugin" -d 'Manage sandboxed WASM scanner plugins' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "completions" -d 'Generate shell completions' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "doctor" -d 'Diagnose the environment (config, git, plugins, tools)' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "man" -d 'Render the roff manual page to stdout' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and __fish_seen_subcommand_from plugin" -f -a "add" -d 'Install a plugin and pin its digest in .ocomment.lock' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and __fish_seen_subcommand_from plugin" -f -a "remove" -d 'Uninstall a plugin and drop its lock entry' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and __fish_seen_subcommand_from plugin" -f -a "list" -d 'List the installed plugins and their pinned digests' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and __fish_seen_subcommand_from plugin" -f -a "update" -d 'Re-fetch plugins and refresh their pinned digests' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and __fish_seen_subcommand_from plugin" -f -a "verify" -d 'Check installed plugins against their pinned digests' +complete -c ocomment -n "__fish_ocomment_using_subcommand help; and __fish_seen_subcommand_from plugin" -f -a "new" -d 'Scaffold a new plugin crate from the scanner WIT world' diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 2a0d93c..14a1ec7 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -241,6 +241,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "clap_mangen" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78" +dependencies = [ + "clap", + "roff", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -821,6 +831,7 @@ dependencies = [ "anyhow", "clap", "clap_complete", + "clap_mangen", "globset", "ignore", "ocomment-core", @@ -838,6 +849,7 @@ dependencies = [ "tokio", "toml", "tower-lsp", + "unicode-width", "wasmi", "wat", "wit-component", @@ -1159,6 +1171,12 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "roff" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189" + [[package]] name = "rustix" version = "1.1.4" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 7906c14..fd67ffd 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -12,8 +12,9 @@ repository = "https://github.com/P4suta/OComment" [workspace.dependencies] anyhow = "1.0.104" -clap = { version = "4.6.6", features = ["derive"] } +clap = { version = "4.6.6", features = ["derive", "string"] } clap_complete = "4.6.8" +clap_mangen = "0.2" globset = "0.4.20" ignore = "0.4.26" memchr = "2.8.0" diff --git a/rust/ocomment-core/src/types.rs b/rust/ocomment-core/src/types.rs index 9cf8df0..153c669 100644 --- a/rust/ocomment-core/src/types.rs +++ b/rust/ocomment-core/src/types.rs @@ -26,6 +26,33 @@ impl ByteSpan { } } +/// Fold a spelling to lower case and drop every `-` and `_`. +fn fold_compact(value: &str) -> String { + value.to_ascii_lowercase().replace(['_', '-'], "") +} + +/// Fold a spelling to lower case and normalise `_` to the canonical `-`. +fn fold_kebab(value: &str) -> String { + value.to_ascii_lowercase().replace('_', "-") +} + +/// Fold a spelling to lower case. +fn fold_lower(value: &str) -> String { + value.to_ascii_lowercase() +} + +/// Find the variant whose canonical name or alias equals the folded spelling. +fn lookup( + all: &[T], + folded: &str, + name: fn(T) -> &'static str, + aliases: fn(T) -> &'static [&'static str], +) -> Option { + all.iter() + .copied() + .find(|value| name(*value) == folded || aliases(*value).contains(&folded)) +} + #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Language { @@ -51,7 +78,8 @@ pub enum Language { } impl Language { - pub const BUILT_INS: [Self; 15] = [ + /// Every CLI-visible language; `Unknown` is deliberately excluded. + pub const ALL: [Self; 15] = [ Self::Rust, Self::Ocaml, Self::C, @@ -69,6 +97,7 @@ impl Language { Self::Kotlin, ]; + /// The canonical name, identical to the serde representation. pub const fn as_str(self) -> &'static str { match self { Self::Rust => "rust", @@ -89,6 +118,25 @@ impl Language { Self::Unknown => "unknown", } } + + /// Accepted spellings besides [`Self::as_str`], already case- and + /// separator-folded. + pub const fn aliases(self) -> &'static [&'static str] { + match self { + Self::Rust => &["rs"], + Self::Ocaml => &["ml"], + Self::C | Self::Java | Self::Css | Self::Sql | Self::Unknown => &[], + Self::Cpp => &["c++", "cxx"], + Self::Go => &["golang"], + Self::JavaScript => &["js", "jsx", "ecmascript"], + Self::TypeScript => &["ts", "tsx"], + Self::Python => &["py"], + Self::Shell => &["sh", "bash", "zsh"], + Self::Html => &["htm"], + Self::Jsonc => &["json5"], + Self::Kotlin => &["kt", "kts"], + } + } } impl fmt::Display for Language { @@ -100,24 +148,13 @@ impl fmt::Display for Language { impl FromStr for Language { type Err = String; fn from_str(value: &str) -> Result { - match value.to_ascii_lowercase().replace(['_', '-'], "").as_str() { - "rust" | "rs" => Ok(Self::Rust), - "ocaml" | "ml" => Ok(Self::Ocaml), - "c" => Ok(Self::C), - "cpp" | "c++" | "cxx" => Ok(Self::Cpp), - "go" | "golang" => Ok(Self::Go), - "java" => Ok(Self::Java), - "javascript" | "js" | "jsx" | "ecmascript" => Ok(Self::JavaScript), - "typescript" | "ts" | "tsx" => Ok(Self::TypeScript), - "python" | "py" => Ok(Self::Python), - "shell" | "sh" | "bash" | "zsh" => Ok(Self::Shell), - "html" | "htm" => Ok(Self::Html), - "css" => Ok(Self::Css), - "jsonc" | "json5" => Ok(Self::Jsonc), - "sql" => Ok(Self::Sql), - "kotlin" | "kt" | "kts" => Ok(Self::Kotlin), - _ => Err(format!("unsupported language `{value}`")), - } + lookup( + &Self::ALL, + &fold_compact(value), + Self::as_str, + Self::aliases, + ) + .ok_or_else(|| format!("unsupported language `{value}`")) } } @@ -152,28 +189,84 @@ pub enum Dialect { Oracle, } +impl Dialect { + /// Every CLI-visible dialect. + pub const ALL: [Self; 16] = [ + Self::Standard, + Self::Jsx, + Self::Tsx, + Self::ObjectiveC, + Self::ObjectiveCpp, + Self::GnuC, + Self::GnuCpp, + Self::Cuda, + Self::PosixSh, + Self::Bash53, + Self::Zsh, + Self::PostgreSql, + Self::MySql, + Self::Sqlite, + Self::TSql, + Self::Oracle, + ]; + + /// The canonical name, identical to the serde representation. + pub const fn as_str(self) -> &'static str { + match self { + Self::Standard => "standard", + Self::Jsx => "jsx", + Self::Tsx => "tsx", + Self::ObjectiveC => "objective-c", + Self::ObjectiveCpp => "objective-cpp", + Self::GnuC => "gnu-c", + Self::GnuCpp => "gnu-cpp", + Self::Cuda => "cuda", + Self::PosixSh => "posix-sh", + Self::Bash53 => "bash53", + Self::Zsh => "zsh", + Self::PostgreSql => "postgresql", + Self::MySql => "mysql", + Self::Sqlite => "sqlite", + Self::TSql => "t-sql", + Self::Oracle => "oracle", + } + } + + /// Accepted spellings besides [`Self::as_str`], already case- and + /// separator-folded. + pub const fn aliases(self) -> &'static [&'static str] { + match self { + Self::Standard + | Self::Jsx + | Self::Tsx + | Self::Cuda + | Self::Zsh + | Self::MySql + | Self::Sqlite + | Self::Oracle => &[], + Self::ObjectiveC => &["objc"], + Self::ObjectiveCpp => &["objective-c++", "objcpp"], + Self::GnuC => &["gnuc"], + Self::GnuCpp => &["gnu-c++", "gnucpp"], + Self::PosixSh => &["posix", "sh"], + Self::Bash53 => &["bash-5.3", "bash"], + Self::PostgreSql => &["postgres", "pgsql"], + Self::TSql => &["tsql"], + } + } +} + +impl fmt::Display for Dialect { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + impl FromStr for Dialect { type Err = String; fn from_str(value: &str) -> Result { - match value.to_ascii_lowercase().replace('_', "-").as_str() { - "standard" => Ok(Self::Standard), - "jsx" => Ok(Self::Jsx), - "tsx" => Ok(Self::Tsx), - "objective-c" | "objc" => Ok(Self::ObjectiveC), - "objective-cpp" | "objective-c++" | "objcpp" => Ok(Self::ObjectiveCpp), - "gnu-c" | "gnuc" => Ok(Self::GnuC), - "gnu-cpp" | "gnu-c++" | "gnucpp" => Ok(Self::GnuCpp), - "cuda" => Ok(Self::Cuda), - "posix-sh" | "posix" | "sh" => Ok(Self::PosixSh), - "bash53" | "bash-5.3" | "bash" => Ok(Self::Bash53), - "zsh" => Ok(Self::Zsh), - "postgresql" | "postgres" | "pgsql" => Ok(Self::PostgreSql), - "mysql" => Ok(Self::MySql), - "sqlite" => Ok(Self::Sqlite), - "t-sql" | "tsql" => Ok(Self::TSql), - "oracle" => Ok(Self::Oracle), - _ => Err(format!("unknown dialect `{value}`")), - } + lookup(&Self::ALL, &fold_kebab(value), Self::as_str, Self::aliases) + .ok_or_else(|| format!("unknown dialect `{value}`")) } } @@ -194,23 +287,69 @@ pub enum CommentKind { VersionComment, } +impl CommentKind { + /// Every CLI-visible comment kind. + pub const ALL: [Self; 11] = [ + Self::Line, + Self::Block, + Self::DocLine, + Self::DocBlock, + Self::Directive, + Self::License, + Self::HtmlComment, + Self::Shebang, + Self::Encoding, + Self::OptimizerHint, + Self::VersionComment, + ]; + + /// The canonical name, identical to the serde representation. + pub const fn as_str(self) -> &'static str { + match self { + Self::Line => "line", + Self::Block => "block", + Self::DocLine => "doc-line", + Self::DocBlock => "doc-block", + Self::Directive => "directive", + Self::License => "license", + Self::HtmlComment => "html-comment", + Self::Shebang => "shebang", + Self::Encoding => "encoding", + Self::OptimizerHint => "optimizer-hint", + Self::VersionComment => "version-comment", + } + } + + /// Accepted spellings besides [`Self::as_str`], already case- and + /// separator-folded. + pub const fn aliases(self) -> &'static [&'static str] { + match self { + Self::Line + | Self::Block + | Self::DocBlock + | Self::Shebang + | Self::Encoding + | Self::OptimizerHint + | Self::VersionComment => &[], + Self::DocLine => &["doc"], + Self::Directive => &["pragma"], + Self::License => &["legal"], + Self::HtmlComment => &["html"], + } + } +} + +impl fmt::Display for CommentKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + impl FromStr for CommentKind { type Err = String; fn from_str(value: &str) -> Result { - match value.to_ascii_lowercase().replace('_', "-").as_str() { - "line" => Ok(Self::Line), - "block" => Ok(Self::Block), - "doc-line" | "doc" => Ok(Self::DocLine), - "doc-block" => Ok(Self::DocBlock), - "directive" | "pragma" => Ok(Self::Directive), - "license" | "legal" => Ok(Self::License), - "html" | "html-comment" => Ok(Self::HtmlComment), - "shebang" => Ok(Self::Shebang), - "encoding" => Ok(Self::Encoding), - "optimizer-hint" => Ok(Self::OptimizerHint), - "version-comment" => Ok(Self::VersionComment), - _ => Err(format!("unknown comment kind `{value}`")), - } + lookup(&Self::ALL, &fold_kebab(value), Self::as_str, Self::aliases) + .ok_or_else(|| format!("unknown comment kind `{value}`")) } } @@ -227,6 +366,15 @@ impl Disposition { } } +impl fmt::Display for Disposition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Remove => f.write_str("remove"), + Self::Keep { reason } => write!(f, "keep ({reason})"), + } + } +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct Comment { pub span: ByteSpan, @@ -244,6 +392,40 @@ pub enum Severity { Hint, } +impl Severity { + /// Every severity, ordered from most to least severe. + pub const ALL: [Self; 4] = [Self::Error, Self::Warning, Self::Info, Self::Hint]; + + /// The canonical name, identical to the serde representation. + pub const fn as_str(self) -> &'static str { + match self { + Self::Error => "error", + Self::Warning => "warning", + Self::Info => "info", + Self::Hint => "hint", + } + } + + /// Accepted spellings besides [`Self::as_str`], already case-folded. + pub const fn aliases(self) -> &'static [&'static str] { + &[] + } +} + +impl fmt::Display for Severity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl FromStr for Severity { + type Err = String; + fn from_str(value: &str) -> Result { + lookup(&Self::ALL, &fold_lower(value), Self::as_str, Self::aliases) + .ok_or_else(|| format!("unknown severity `{value}`")) + } +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct Diagnostic { pub code: String, @@ -289,15 +471,36 @@ pub enum Policy { All, } +impl Policy { + /// Every CLI-visible policy. + pub const ALL: [Self; 3] = [Self::Safe, Self::Legal, Self::All]; + + /// The canonical name, identical to the serde representation. + pub const fn as_str(self) -> &'static str { + match self { + Self::Safe => "safe", + Self::Legal => "legal", + Self::All => "all", + } + } + + /// Accepted spellings besides [`Self::as_str`], already case-folded. + pub const fn aliases(self) -> &'static [&'static str] { + &[] + } +} + +impl fmt::Display for Policy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + impl FromStr for Policy { type Err = String; fn from_str(value: &str) -> Result { - match value.to_ascii_lowercase().as_str() { - "safe" => Ok(Self::Safe), - "legal" => Ok(Self::Legal), - "all" => Ok(Self::All), - _ => Err(format!("unknown policy `{value}`")), - } + lookup(&Self::ALL, &fold_lower(value), Self::as_str, Self::aliases) + .ok_or_else(|| format!("unknown policy `{value}`")) } } @@ -310,15 +513,36 @@ pub enum Layout { Compact, } +impl Layout { + /// Every CLI-visible layout. + pub const ALL: [Self; 3] = [Self::Lines, Self::Columns, Self::Compact]; + + /// The canonical name, identical to the serde representation. + pub const fn as_str(self) -> &'static str { + match self { + Self::Lines => "lines", + Self::Columns => "columns", + Self::Compact => "compact", + } + } + + /// Accepted spellings besides [`Self::as_str`], already case-folded. + pub const fn aliases(self) -> &'static [&'static str] { + &[] + } +} + +impl fmt::Display for Layout { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + impl FromStr for Layout { type Err = String; fn from_str(value: &str) -> Result { - match value.to_ascii_lowercase().as_str() { - "lines" => Ok(Self::Lines), - "columns" => Ok(Self::Columns), - "compact" => Ok(Self::Compact), - _ => Err(format!("unknown layout `{value}`")), - } + lookup(&Self::ALL, &fold_lower(value), Self::as_str, Self::aliases) + .ok_or_else(|| format!("unknown layout `{value}`")) } } diff --git a/rust/ocomment-core/tests/names.rs b/rust/ocomment-core/tests/names.rs new file mode 100644 index 0000000..ddb5fd4 --- /dev/null +++ b/rust/ocomment-core/tests/names.rs @@ -0,0 +1,395 @@ +//! Stable-name contract for the public enums. +//! +//! `as_str` is the single source of truth for every user-visible spelling: it +//! must equal the serde name byte-for-byte, round-trip through `FromStr`, and +//! agree with `Display`. Every historical alias is pinned here so a refactor +//! cannot silently drop one. + +use ocomment_core::{ + CommentKind, Dialect, Disposition, Language, Layout, Policy, ScanOptions, Severity, scan, +}; +use std::{collections::BTreeSet, str::FromStr}; + +fn serde_name(value: &T) -> String { + serde_json::to_value(value) + .expect("enum serializes") + .as_str() + .expect("enum serializes as a string") + .to_owned() +} + +/// Every variant of `$type` agrees with serde, `FromStr`, and `Display`, and no +/// spelling is claimed by two variants. +macro_rules! check_stable_names { + ($type:ident) => {{ + let mut seen = BTreeSet::new(); + for value in $type::ALL { + assert_eq!( + serde_name(&value), + value.as_str(), + "{}::{value:?} serde name differs from as_str", + stringify!($type) + ); + assert_eq!( + $type::from_str(value.as_str()), + Ok(value), + "{}::{value:?} canonical name does not round-trip", + stringify!($type) + ); + assert_eq!( + value.to_string(), + value.as_str(), + "{}::{value:?} Display differs from as_str", + stringify!($type) + ); + assert!( + seen.insert(value.as_str()), + "{}::{value:?} name `{}` is claimed twice", + stringify!($type), + value.as_str() + ); + for alias in value.aliases() { + assert_eq!( + $type::from_str(alias), + Ok(value), + "{}::{value:?} alias `{alias}` does not parse", + stringify!($type) + ); + assert!( + seen.insert(alias), + "{}::{value:?} alias `{alias}` is claimed twice", + stringify!($type) + ); + } + } + seen + }}; +} + +#[test] +fn language_names_are_stable() { + let seen = check_stable_names!(Language); + assert_eq!(Language::ALL.len(), 15); + assert!( + !seen.contains("unknown"), + "Unknown must stay out of the parseable set" + ); + assert_eq!(serde_name(&Language::Unknown), "unknown"); + assert_eq!(Language::Unknown.as_str(), "unknown"); + assert_eq!(Language::Unknown.to_string(), "unknown"); + assert!(!Language::ALL.contains(&Language::Unknown)); +} + +#[test] +fn dialect_names_are_stable() { + check_stable_names!(Dialect); + assert_eq!(Dialect::ALL.len(), 16); +} + +#[test] +fn comment_kind_names_are_stable() { + check_stable_names!(CommentKind); + assert_eq!(CommentKind::ALL.len(), 11); +} + +#[test] +fn policy_names_are_stable() { + check_stable_names!(Policy); + assert_eq!(Policy::ALL.len(), 3); +} + +#[test] +fn layout_names_are_stable() { + check_stable_names!(Layout); + assert_eq!(Layout::ALL.len(), 3); +} + +#[test] +fn severity_names_are_stable() { + check_stable_names!(Severity); + assert_eq!(Severity::ALL.len(), 4); +} + +#[test] +fn language_aliases_are_pinned() { + let cases = [ + ("rust", Language::Rust), + ("rs", Language::Rust), + ("ocaml", Language::Ocaml), + ("ml", Language::Ocaml), + ("c", Language::C), + ("cpp", Language::Cpp), + ("c++", Language::Cpp), + ("cxx", Language::Cpp), + ("go", Language::Go), + ("golang", Language::Go), + ("java", Language::Java), + ("javascript", Language::JavaScript), + ("js", Language::JavaScript), + ("jsx", Language::JavaScript), + ("ecmascript", Language::JavaScript), + ("typescript", Language::TypeScript), + ("ts", Language::TypeScript), + ("tsx", Language::TypeScript), + ("python", Language::Python), + ("py", Language::Python), + ("shell", Language::Shell), + ("sh", Language::Shell), + ("bash", Language::Shell), + ("zsh", Language::Shell), + ("html", Language::Html), + ("htm", Language::Html), + ("css", Language::Css), + ("jsonc", Language::Jsonc), + ("json5", Language::Jsonc), + ("sql", Language::Sql), + ("kotlin", Language::Kotlin), + ("kt", Language::Kotlin), + ("kts", Language::Kotlin), + ]; + for (text, expected) in cases { + assert_eq!(Language::from_str(text), Ok(expected), "`{text}`"); + } +} + +#[test] +fn language_parsing_ignores_case_dashes_and_underscores() { + for text in ["RUST", "Rust", "-r-u-s-t-", "r_u_s_t"] { + assert_eq!(Language::from_str(text), Ok(Language::Rust), "`{text}`"); + } + assert_eq!( + Language::from_str("Java_Script"), + Ok(Language::JavaScript), + "underscores are stripped" + ); + assert_eq!(Language::from_str("C++"), Ok(Language::Cpp)); +} + +#[test] +fn dialect_aliases_are_pinned() { + let cases = [ + ("standard", Dialect::Standard), + ("jsx", Dialect::Jsx), + ("tsx", Dialect::Tsx), + ("objective-c", Dialect::ObjectiveC), + ("objc", Dialect::ObjectiveC), + ("objective-cpp", Dialect::ObjectiveCpp), + ("objective-c++", Dialect::ObjectiveCpp), + ("objcpp", Dialect::ObjectiveCpp), + ("gnu-c", Dialect::GnuC), + ("gnuc", Dialect::GnuC), + ("gnu-cpp", Dialect::GnuCpp), + ("gnu-c++", Dialect::GnuCpp), + ("gnucpp", Dialect::GnuCpp), + ("cuda", Dialect::Cuda), + ("posix-sh", Dialect::PosixSh), + ("posix", Dialect::PosixSh), + ("sh", Dialect::PosixSh), + ("bash53", Dialect::Bash53), + ("bash-5.3", Dialect::Bash53), + ("bash", Dialect::Bash53), + ("zsh", Dialect::Zsh), + ("postgresql", Dialect::PostgreSql), + ("postgres", Dialect::PostgreSql), + ("pgsql", Dialect::PostgreSql), + ("mysql", Dialect::MySql), + ("sqlite", Dialect::Sqlite), + ("t-sql", Dialect::TSql), + ("tsql", Dialect::TSql), + ("oracle", Dialect::Oracle), + ]; + for (text, expected) in cases { + assert_eq!(Dialect::from_str(text), Ok(expected), "`{text}`"); + } +} + +#[test] +fn dialect_parsing_folds_case_and_underscores() { + assert_eq!(Dialect::from_str("Objective_C"), Ok(Dialect::ObjectiveC)); + assert_eq!(Dialect::from_str("GNU-CPP"), Ok(Dialect::GnuCpp)); + assert_eq!(Dialect::from_str("bash_5.3"), Ok(Dialect::Bash53)); + assert_eq!(Dialect::from_str("T_SQL"), Ok(Dialect::TSql)); +} + +#[test] +fn comment_kind_aliases_are_pinned() { + let cases = [ + ("line", CommentKind::Line), + ("block", CommentKind::Block), + ("doc-line", CommentKind::DocLine), + ("doc", CommentKind::DocLine), + ("doc-block", CommentKind::DocBlock), + ("directive", CommentKind::Directive), + ("pragma", CommentKind::Directive), + ("license", CommentKind::License), + ("legal", CommentKind::License), + ("html", CommentKind::HtmlComment), + ("html-comment", CommentKind::HtmlComment), + ("shebang", CommentKind::Shebang), + ("encoding", CommentKind::Encoding), + ("optimizer-hint", CommentKind::OptimizerHint), + ("version-comment", CommentKind::VersionComment), + ]; + for (text, expected) in cases { + assert_eq!(CommentKind::from_str(text), Ok(expected), "`{text}`"); + } +} + +#[test] +fn comment_kind_parsing_folds_case_and_underscores() { + assert_eq!( + CommentKind::from_str("DOC_BLOCK"), + Ok(CommentKind::DocBlock) + ); + assert_eq!( + CommentKind::from_str("Optimizer_Hint"), + Ok(CommentKind::OptimizerHint) + ); + assert_eq!( + CommentKind::from_str("HTML_COMMENT"), + Ok(CommentKind::HtmlComment) + ); +} + +#[test] +fn policy_and_layout_aliases_are_pinned() { + assert_eq!(Policy::from_str("safe"), Ok(Policy::Safe)); + assert_eq!(Policy::from_str("legal"), Ok(Policy::Legal)); + assert_eq!(Policy::from_str("all"), Ok(Policy::All)); + assert_eq!(Policy::from_str("SAFE"), Ok(Policy::Safe)); + assert_eq!(Layout::from_str("lines"), Ok(Layout::Lines)); + assert_eq!(Layout::from_str("columns"), Ok(Layout::Columns)); + assert_eq!(Layout::from_str("compact"), Ok(Layout::Compact)); + assert_eq!(Layout::from_str("Compact"), Ok(Layout::Compact)); + assert!(Policy::ALL.iter().all(|value| value.aliases().is_empty())); + assert!(Layout::ALL.iter().all(|value| value.aliases().is_empty())); +} + +#[test] +fn rejection_messages_are_unchanged() { + assert_eq!( + Language::from_str("unknown"), + Err("unsupported language `unknown`".to_owned()) + ); + assert_eq!( + Language::from_str("Klingon"), + Err("unsupported language `Klingon`".to_owned()) + ); + assert_eq!( + Dialect::from_str("mariadb"), + Err("unknown dialect `mariadb`".to_owned()) + ); + assert_eq!( + CommentKind::from_str("footnote"), + Err("unknown comment kind `footnote`".to_owned()) + ); + assert_eq!( + Policy::from_str("paranoid"), + Err("unknown policy `paranoid`".to_owned()) + ); + assert_eq!( + Layout::from_str("grid"), + Err("unknown layout `grid`".to_owned()) + ); +} + +#[test] +fn disposition_display_is_human_readable() { + assert_eq!(Disposition::Remove.to_string(), "remove"); + assert_eq!( + Disposition::Keep { + reason: "legal policy".to_owned() + } + .to_string(), + "keep (legal policy)" + ); +} + +#[test] +fn disposition_serde_shape_is_frozen() { + assert_eq!( + serde_json::to_value(Disposition::Remove).unwrap(), + serde_json::json!({"action": "remove"}) + ); + assert_eq!( + serde_json::to_value(Disposition::Keep { + reason: "legal policy".to_owned() + }) + .unwrap(), + serde_json::json!({"action": "keep", "reason": "legal policy"}) + ); +} + +/// The differential protocol freezes these five strings; the OCaml reference +/// compares them byte-for-byte. +const KEEP_REASONS: [&str; 5] = [ + "kept by kind or regex override", + "required source preamble", + "HTML comments are DOM-observable", + "tool or language directive", + "legal policy", +]; + +#[test] +fn keep_reasons_are_observable_through_scan() { + let cases: [(&[u8], Language, ScanOptions, &str); 5] = [ + ( + b"// keep me\n", + Language::Rust, + ScanOptions { + keep_kinds: vec![CommentKind::Line], + ..Default::default() + }, + "kept by kind or regex override", + ), + ( + b"#!/bin/sh\n", + Language::Shell, + ScanOptions::default(), + "required source preamble", + ), + ( + b"\n", + Language::Html, + ScanOptions::default(), + "HTML comments are DOM-observable", + ), + ( + b"// rustfmt::skip\n", + Language::Rust, + ScanOptions::default(), + "tool or language directive", + ), + ( + b"// Copyright 2026 Example\n", + Language::Rust, + ScanOptions { + policy: Policy::Legal, + ..Default::default() + }, + "legal policy", + ), + ]; + let mut observed = BTreeSet::new(); + for (source, language, options, expected) in cases { + observed.insert(expected); + let report = scan(source, language, options); + assert_eq!(report.comments.len(), 1, "`{expected}` fixture"); + assert_eq!( + report.comments[0].disposition, + Disposition::Keep { + reason: expected.to_owned() + }, + "`{expected}` fixture" + ); + assert_eq!( + report.comments[0].disposition.to_string(), + format!("keep ({expected})") + ); + } + assert_eq!( + observed, + BTreeSet::from(KEEP_REASONS), + "the fixtures no longer exercise every frozen keep reason" + ); +} diff --git a/rust/ocomment-core/tests/properties.rs b/rust/ocomment-core/tests/properties.rs index 997df5b..467ae9a 100644 --- a/rust/ocomment-core/tests/properties.rs +++ b/rust/ocomment-core/tests/properties.rs @@ -89,7 +89,7 @@ proptest! { let left = first % modulus; let right = second % modulus; let span = ByteSpan::new(left.min(right), left.max(right)); - for language in Language::BUILT_INS { + for language in Language::ALL { let mut document = IncrementalDocument::new( source.clone(), language, diff --git a/rust/ocomment/Cargo.toml b/rust/ocomment/Cargo.toml index 95bcb40..9f163a1 100644 --- a/rust/ocomment/Cargo.toml +++ b/rust/ocomment/Cargo.toml @@ -30,6 +30,7 @@ path = "src/main.rs" anyhow.workspace = true clap.workspace = true clap_complete.workspace = true +clap_mangen.workspace = true globset.workspace = true ignore.workspace = true ocomment-core = { version = "0.1.0", path = "../ocomment-core" } @@ -45,6 +46,7 @@ thiserror.workspace = true toml.workspace = true tokio.workspace = true tower-lsp.workspace = true +unicode-width.workspace = true wasm_component_layer.workspace = true wasmi.workspace = true wasmi_runtime_layer.workspace = true diff --git a/rust/ocomment/src/cli.rs b/rust/ocomment/src/cli.rs index d45e772..5c8cf76 100644 --- a/rust/ocomment/src/cli.rs +++ b/rust/ocomment/src/cli.rs @@ -1,13 +1,16 @@ use crate::{ atomic::{WritePlan, apply_transaction}, config, files, git, lsp, - output::{self, Operation, OutputFormat, Presentation, ProcessedFile}, + output::{ + self, Operation, OutputFormat, Presentation, ProcessedFile, RenderOptions, Verbosity, + }, plugin, + values::{CommentKindArg, DialectArg, LanguageArg, LayoutArg, PolicyArg}, }; use anyhow::{Context, Result}; use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum}; use clap_complete::{Shell, generate}; -use ocomment_core::{CommentKind, Dialect, Language, Layout, Policy, transform}; +use ocomment_core::{CommentKind, Dialect, Language, transform}; use rayon::prelude::*; use std::{ fs, @@ -15,53 +18,191 @@ use std::{ path::PathBuf, }; +const LONG_ABOUT: &str = "\ +OComment scans source bytes without requiring UTF-8 and reports or removes \ +comment tokens. The default policy protects source preambles and tool or \ +language directives. Rewrites are prepared and committed as one \ +rollback-backed transaction."; + +const AFTER_LONG_HELP: &str = "\ +EXIT STATUS + 0 Nothing removable was found and every requested change was applied. + 1 Removable comments were reported, or a diff was printed. + 2 Invalid source, configuration, plugin, or I/O failure. + +FILES + .ocomment.toml Project configuration, merged over the user file. + .ocommentignore Extra ignore patterns honoured by repository walks. + .ocomment.lock Pinned digests of the installed WASM scanner plugins. + $XDG_CONFIG_HOME/ocomment/config.toml + User configuration, merged over the built-in defaults. + +EXAMPLES + ocomment + Check the current repository and report removable comments. + ocomment fix --policy all --layout compact src + Remove every comment under src and close the gaps it leaves. + ocomment strip --language rust < before.rs > after.rs + Strip one file from standard input to standard output. + +SEE ALSO + The complete schemas and guides are available in the OComment repository."; + #[derive(Parser)] #[command( name = "ocomment", version, about = "Check and remove source-code comments safely" )] +#[command(long_about = LONG_ABOUT)] #[command(args_conflicts_with_subcommands = true)] +#[command(after_long_help = AFTER_LONG_HELP)] struct Cli { - #[command(flatten)] - common: CommonArgs, - #[command(subcommand)] - command: Option, - /// Paths for the implicit `check` command. + /// Files or directories to check (default: current directory). #[arg(value_name = "PATH")] paths: Vec, + /// The command to run; `check` runs when none is given. + #[command(subcommand)] + command: Option, + /// Configuration, policy, and output options shared by every command. + #[command(flatten)] + common: CommonArgs, } #[derive(Clone, Debug, Args)] struct CommonArgs { - /// Explicit configuration file. - #[arg(long, global = true)] + /// Read this configuration file instead of discovering `.ocomment.toml`. + #[arg(long, global = true, value_name = "FILE")] config: Option, - /// Output encoding. - #[arg(long, global = true, value_enum, default_value_t)] - format: OutputFormat, - #[arg(long, global = true)] - policy: Option, - #[arg(long, global = true)] - layout: Option, - #[arg(long, global = true)] - language: Option, - #[arg(long, global = true)] - dialect: Option, - #[arg(long = "keep-kind", global = true, value_delimiter = ',')] - keep_kind: Vec, - #[arg(long = "remove-kind", global = true, value_delimiter = ',')] - remove_kind: Vec, + /// What may be removed and how the source is interpreted. + #[command(flatten)] + policy: PolicyArgs, + /// How results are encoded and decorated. + #[command(flatten)] + output: OutputArgs, +} + +#[derive(Clone, Debug, Args)] +#[command(next_help_heading = "Policy")] +struct PolicyArgs { + /// Which classes of comment the run is allowed to remove. + #[arg( + long, + global = true, + value_enum, + ignore_case = true, + value_name = "POLICY" + )] + policy: Option, + /// How the bytes left behind by a removed comment are laid out. + #[arg( + long, + global = true, + value_enum, + ignore_case = true, + value_name = "LAYOUT" + )] + layout: Option, + /// Force this language instead of detecting it from path and contents. + #[arg( + long, + global = true, + value_enum, + ignore_case = true, + value_name = "LANGUAGE" + )] + language: Option, + /// Force this dialect of the selected language. + #[arg( + long, + global = true, + value_enum, + ignore_case = true, + value_name = "DIALECT" + )] + dialect: Option, + /// Comma-separated comment kinds to protect on top of the policy. + #[arg( + long = "keep-kind", + global = true, + value_enum, + ignore_case = true, + value_delimiter = ',', + value_name = "KIND" + )] + keep_kind: Vec, + /// Comma-separated comment kinds to remove regardless of the policy. + #[arg( + long = "remove-kind", + global = true, + value_enum, + ignore_case = true, + value_delimiter = ',', + value_name = "KIND" + )] + remove_kind: Vec, + /// Apply the edits that are still provably safe when the source fails to scan. #[arg(long, global = true)] force_invalid: bool, + /// Remove protected comments such as shebang and encoding preambles. #[arg(long, global = true)] force_protected: bool, - #[arg(long, global = true, value_enum, default_value_t)] +} + +#[derive(Clone, Debug, Args)] +#[command(next_help_heading = "Output")] +struct OutputArgs { + /// Output encoding. + #[arg( + long, + global = true, + value_enum, + default_value_t, + value_name = "FORMAT" + )] + format: OutputFormat, + /// When to colour terminal output. + #[arg(long, global = true, value_enum, default_value_t, value_name = "WHEN")] color: ColorChoice, - #[arg(long, global = true, value_enum, default_value_t)] + /// When to emit terminal hyperlinks for reported paths. + #[arg(long, global = true, value_enum, default_value_t, value_name = "WHEN")] hyperlinks: AutoChoice, - #[arg(long, global = true, value_enum, default_value_t)] + /// Omit the one-line comment text from human `check` and `scan` lines. + #[arg(long, global = true)] + no_preview: bool, + /// Accepted for compatibility; the end-of-run summary replaced this line. + // Nothing reads the value: the summary is written whether or not standard + // error is a terminal. + #[allow(dead_code)] + #[arg(long, global = true, value_enum, default_value_t, value_name = "WHEN")] progress: AutoChoice, + /// Print nothing but errors and diagnostics. + #[arg(short, long, global = true, conflicts_with = "verbose")] + quiet: bool, + /// Trace what is scanned and summarize every comment kind and skipped file. + #[arg(short, long, global = true)] + verbose: bool, +} + +impl CommonArgs { + /// The language forced on the command line, if any. + fn language(&self) -> Option { + self.policy.language.map(Language::from) + } + + /// The dialect forced on the command line, if any. + fn dialect(&self) -> Option { + self.policy.dialect.map(Dialect::from) + } + + /// How much of the human report this run may write. + fn verbosity(&self) -> Verbosity { + match (self.output.quiet, self.output.verbose) { + (true, _) => Verbosity::Quiet, + (_, true) => Verbosity::Verbose, + _ => Verbosity::Normal, + } + } } #[derive(Clone, Copy, Debug, Default, ValueEnum)] @@ -82,22 +223,40 @@ enum AutoChoice { #[derive(Subcommand)] enum Command { + /// Report removable comments (default command) Check(TargetArgs), + /// Remove comments in place through an atomic, rollback-backed transaction Fix(TargetArgs), + /// Print a unified diff of the changes fix would make Diff(TargetArgs), + /// List every comment with its kind, disposition and byte span Scan(TargetArgs), + /// Read source on stdin and write the stripped result to stdout Strip, + /// Run the LSP 3.18 server over stdio Lsp, + /// Write a starter .ocomment.toml or Lefthook configuration Init(InitArgs), + /// Show, locate, explain, or export the resolved configuration Config(ConfigArgs), + /// List built-in languages, extensions, and dialects Languages, + /// Manage sandboxed WASM scanner plugins Plugin(PluginArgs), - Completions { shell: Shell }, + /// Generate shell completions + Completions { + /// Shell whose completion script is written to stdout. + shell: Shell, + }, + /// Diagnose the environment (config, git, plugins, tools) Doctor, + /// Render the roff manual page to stdout + Man, } #[derive(Clone, Debug, Default, Args)] struct TargetArgs { + /// Files or directories to process (default: current directory). #[arg(value_name = "PATH")] paths: Vec, /// Read and update Git index blobs rather than treating the working tree as the source. @@ -110,8 +269,10 @@ struct TargetArgs { #[derive(Args)] struct InitArgs { + /// Which starter file to write. #[arg(value_enum, default_value_t)] kind: InitKind, + /// For the Lefthook hook, run `fix` instead of `check`. #[arg(long)] fix: bool, } @@ -125,6 +286,7 @@ enum InitKind { #[derive(Args)] struct ConfigArgs { + /// Which view of the resolved configuration to print. #[arg(value_enum, default_value_t)] action: ConfigAction, } @@ -140,32 +302,47 @@ enum ConfigAction { #[derive(Args)] struct PluginArgs { + /// The plugin operation to run. #[command(subcommand)] command: PluginCommand, } #[derive(Subcommand)] enum PluginCommand { + /// Install a plugin and pin its digest in .ocomment.lock Add { + /// Path or URL of the WASM component to install. source: String, - #[arg(long)] + /// Name to register the plugin under (default: the file stem). + #[arg(long, value_name = "NAME")] name: Option, - #[arg(long)] + /// Expected SHA-256 digest of the component, verified before install. + #[arg(long, value_name = "HEX")] sha256: Option, - #[arg(long)] + /// Publisher identity recorded alongside the pinned digest. + #[arg(long, value_name = "IDENTITY")] identity: Option, }, + /// Uninstall a plugin and drop its lock entry Remove { + /// Name of the plugin to remove. name: String, }, + /// List the installed plugins and their pinned digests List, + /// Re-fetch plugins and refresh their pinned digests Update { + /// Name of the plugin to update (default: all of them). name: Option, }, + /// Check installed plugins against their pinned digests Verify { + /// Name of the plugin to verify (default: all of them). name: Option, }, + /// Scaffold a new plugin crate from the scanner WIT world New { + /// Directory to create the plugin crate in. path: PathBuf, }, } @@ -200,6 +377,7 @@ pub fn run() -> Result { Ok(0) } Some(Command::Doctor) => run_doctor(&common), + Some(Command::Man) => run_man(), } } @@ -208,31 +386,37 @@ fn run_target(operation: Operation, args: TargetArgs, common: &CommonArgs) -> Re apply_cli_overrides(&mut resolved.config, common); let plugin_host = plugin::PluginHost::load(&resolved.root, &resolved.config.plugins)?; let presentation = presentation(common); + let verbosity = common.verbosity(); + if verbosity == Verbosity::Verbose { + trace_run(&resolved, &args.paths); + } let staged = args.staged || resolved.config.git.staged; if staged { return git::run_staged(git::StagedRequest { operation, paths: &args.paths, resolved: &resolved, - format: common.format, + format: common.output.format, index_only: args.index_only || resolved.config.git.index_only, plugin_host: &plugin_host, - forced_language: common.language, - forced_dialect: common.dialect, + forced_language: common.language(), + forced_dialect: common.dialect(), presentation, + verbosity, + preview: !common.output.no_preview, }); } - let discovery = files::discover(&args.paths, &resolved, common.language, common.dialect)?; + let discovery = files::discover(&args.paths, &resolved, common.language(), common.dialect())?; let files: Vec<_> = discovery .files .par_iter() .map(|file| { let (mut language, mut options) = resolved.for_path(&file.path, file.language, file.dialect); - if let Some(value) = common.language { + if let Some(value) = common.language() { language = value; } - if let Some(value) = common.dialect { + if let Some(value) = common.dialect() { config::validate_dialect(language, value)?; options.scan.dialect = value; } @@ -263,7 +447,8 @@ fn run_target(operation: Operation, args: TargetArgs, common: &CommonArgs) -> Re let io_invalid = discovery.skipped.iter().any(|item| item.error); let invalid = report_invalid || io_invalid; let may_fix = !io_invalid && (!report_invalid || resolved.config.policy.force_invalid); - if operation == Operation::Fix && may_fix { + let applied = operation == Operation::Fix && may_fix; + if applied { let plans = files .iter() .filter(|file| file.source != file.result.output) @@ -278,9 +463,17 @@ fn run_target(operation: Operation, args: TargetArgs, common: &CommonArgs) -> Re output::render( &files, &discovery.skipped, - common.format, - operation, - presentation, + &RenderOptions { + format: common.output.format, + operation, + presentation, + verbosity, + preview: !common.output.no_preview, + explain: false, + dry_run: false, + force_invalid: resolved.config.policy.force_invalid, + applied, + }, )?; if invalid { return Ok(2); @@ -300,8 +493,8 @@ fn run_strip(common: &CommonArgs) -> Result { let mut resolved = config::load(common.config.as_deref())?; apply_cli_overrides(&mut resolved.config, common); let detection = common - .language - .map(|language| (language, common.dialect.unwrap_or(Dialect::Standard))) + .language() + .map(|language| (language, common.dialect().unwrap_or(Dialect::Standard))) .or_else(|| { ocomment_core::detect_language(None, &source) .map(|value| (value.language, value.dialect)) @@ -309,7 +502,7 @@ fn run_strip(common: &CommonArgs) -> Result { .context("cannot detect stdin language; pass --language")?; let (language, mut options) = resolved.for_path(std::path::Path::new(""), detection.0, detection.1); - if let Some(value) = common.dialect { + if let Some(value) = common.dialect() { config::validate_dialect(language, value)?; options.scan.dialect = value; } @@ -331,32 +524,48 @@ fn run_strip(common: &CommonArgs) -> Result { } fn apply_cli_overrides(config: &mut config::Config, common: &CommonArgs) { - if let Some(value) = common.policy { - config.policy.mode = value; + let policy = &common.policy; + if let Some(value) = policy.policy { + config.policy.mode = *value; } - if let Some(value) = common.layout { - config.policy.layout = value; + if let Some(value) = policy.layout { + config.policy.layout = *value; } - if !common.keep_kind.is_empty() { + if !policy.keep_kind.is_empty() { config .policy .keep_kind - .extend(common.keep_kind.iter().copied()); + .extend(policy.keep_kind.iter().copied().map(CommentKind::from)); } - if !common.remove_kind.is_empty() { + if !policy.remove_kind.is_empty() { config .policy .remove_kind - .extend(common.remove_kind.iter().copied()); + .extend(policy.remove_kind.iter().copied().map(CommentKind::from)); } - if common.force_invalid { + if policy.force_invalid { config.policy.force_invalid = true; } - if common.force_protected { + if policy.force_protected { config.policy.force_protected = true; } } +/// Render the roff manual page from the parser definition itself. +fn run_man() -> Result { + let mut page = Vec::new(); + clap_mangen::Man::new(Cli::command()) + .title("OCOMMENT") + .manual("User Commands") + .render(&mut page) + .context("cannot render the manual page")?; + io::stdout() + .lock() + .write_all(&page) + .context("cannot write standard output")?; + Ok(0) +} + fn run_init(args: InitArgs) -> Result { match args.kind { InitKind::Config => create_new( @@ -421,7 +630,7 @@ fn run_config(args: ConfigArgs, common: &CommonArgs) -> Result { println!("precedence: built-in < XDG user < project < path override < CLI"); println!("root: {}", resolved.root.display()); println!( - "policy: {:?}; layout: {:?}", + "policy: {}; layout: {}", resolved.config.policy.mode, resolved.config.policy.layout ); } @@ -479,7 +688,7 @@ fn run_doctor(common: &CommonArgs) -> Result { println!("ocomment {}", env!("CARGO_PKG_VERSION")); let resolved = config::load(common.config.as_deref())?; println!("configuration: ok (root {})", resolved.root.display()); - println!("languages: {} built in", Language::BUILT_INS.len()); + println!("languages: {} built in", Language::ALL.len()); if std::process::Command::new("git") .arg("--version") .output() @@ -499,24 +708,50 @@ fn run_doctor(common: &CommonArgs) -> Result { fn presentation(common: &CommonArgs) -> Presentation { let stdout_tty = io::stdout().is_terminal(); - let stderr_tty = io::stderr().is_terminal(); let no_color = std::env::var_os("NO_COLOR").is_some(); Presentation { color: !no_color - && match common.color { + && match common.output.color { ColorChoice::Auto => stdout_tty, ColorChoice::Always => true, ColorChoice::Never => false, }, - hyperlinks: match common.hyperlinks { + hyperlinks: match common.output.hyperlinks { AutoChoice::Auto => stdout_tty, AutoChoice::Always => true, AutoChoice::Never => false, }, - progress: match common.progress { - AutoChoice::Auto => stderr_tty, - AutoChoice::Always => true, - AutoChoice::Never => false, - }, + } +} + +/// The `--verbose` header: where the run is rooted, what it was pointed at, +/// and which configuration files it merged. +fn trace_run(resolved: &config::ResolvedConfig, paths: &[PathBuf]) { + eprintln!("root: {}", resolved.root.display()); + let target = if paths.is_empty() { + ".".to_owned() + } else { + paths + .iter() + .map(|path| path.display().to_string()) + .collect::>() + .join(" ") + }; + eprintln!("target: {target}"); + let trace = &resolved.trace; + let sources = [ + ("user", &trace.user), + ("project", &trace.project), + ("explicit", &trace.explicit), + ]; + let mut traced = false; + for (label, path) in sources { + if let Some(path) = path { + eprintln!("config: {label} {}", path.display()); + traced = true; + } + } + if !traced { + eprintln!("config: built-in defaults"); } } diff --git a/rust/ocomment/src/config.rs b/rust/ocomment/src/config.rs index d75e262..114c37c 100644 --- a/rust/ocomment/src/config.rs +++ b/rust/ocomment/src/config.rs @@ -295,36 +295,46 @@ fn validate_languages(config: &Config) -> Result<()> { Ok(()) } +/// Every dialect the scanner accepts for `language`, in canonical order. +pub fn supported_dialects(language: Language) -> &'static [Dialect] { + match language { + Language::JavaScript => &[Dialect::Standard, Dialect::Jsx], + Language::TypeScript => &[Dialect::Standard, Dialect::Tsx], + Language::C => &[Dialect::Standard, Dialect::ObjectiveC, Dialect::GnuC], + Language::Cpp => &[ + Dialect::Standard, + Dialect::ObjectiveCpp, + Dialect::GnuCpp, + Dialect::Cuda, + ], + Language::Shell => &[ + Dialect::Standard, + Dialect::PosixSh, + Dialect::Bash53, + Dialect::Zsh, + ], + Language::Sql => &[ + Dialect::Standard, + Dialect::PostgreSql, + Dialect::MySql, + Dialect::Sqlite, + Dialect::TSql, + Dialect::Oracle, + ], + _ => &[Dialect::Standard], + } +} + pub fn validate_dialect(language: Language, dialect: Dialect) -> Result<()> { - let compatible = match language { - Language::JavaScript => matches!(dialect, Dialect::Standard | Dialect::Jsx), - Language::TypeScript => matches!(dialect, Dialect::Standard | Dialect::Tsx), - Language::C => matches!( - dialect, - Dialect::Standard | Dialect::GnuC | Dialect::ObjectiveC - ), - Language::Cpp => matches!( - dialect, - Dialect::Standard | Dialect::GnuCpp | Dialect::ObjectiveCpp | Dialect::Cuda - ), - Language::Shell => matches!( - dialect, - Dialect::Standard | Dialect::PosixSh | Dialect::Bash53 | Dialect::Zsh - ), - Language::Sql => matches!( - dialect, - Dialect::Standard - | Dialect::PostgreSql - | Dialect::MySql - | Dialect::Sqlite - | Dialect::TSql - | Dialect::Oracle - ), - _ => dialect == Dialect::Standard, - }; + let supported = supported_dialects(language); ensure!( - compatible, - "dialect `{dialect:?}` is not supported for language `{language}`" + supported.contains(&dialect), + "unsupported dialect `{dialect}` for {language}; supported: {}", + supported + .iter() + .map(|value| value.as_str()) + .collect::>() + .join(", ") ); Ok(()) } diff --git a/rust/ocomment/src/files.rs b/rust/ocomment/src/files.rs index 8425f09..8b38806 100644 --- a/rust/ocomment/src/files.rs +++ b/rust/ocomment/src/files.rs @@ -23,6 +23,10 @@ pub struct SkippedFile { pub path: PathBuf, pub reason: String, pub error: bool, + /// The path itself was named on the command line. Such a skip is always + /// reported on its own line; a skip found while walking a directory is + /// folded into the end-of-run summary instead. + pub explicit: bool, } #[derive(Default)] @@ -74,6 +78,7 @@ fn discover_with_scope( load_one( &path, explicit_scope, + explicit_scope, resolved, forced_language, forced_dialect, @@ -104,6 +109,7 @@ fn discover_with_scope( load_one( entry.path(), explicit_scope, + false, resolved, forced_language, forced_dialect, @@ -117,6 +123,7 @@ fn discover_with_scope( path: path.clone(), reason: error.to_string(), error: true, + explicit: explicit_scope, }), } } @@ -125,6 +132,7 @@ fn discover_with_scope( path, reason: "path does not exist".into(), error: true, + explicit: explicit_scope, }); } } @@ -144,6 +152,7 @@ fn discover_with_scope( fn load_one( path: &Path, explicit_scope: bool, + explicit_path: bool, resolved: &ResolvedConfig, forced_language: Option, forced_dialect: Option, @@ -158,7 +167,7 @@ fn load_one( let link_metadata = match path.symlink_metadata() { Ok(value) => value, Err(error) => { - discovery.skipped.push(skip(path, error)); + discovery.skipped.push(skip(path, explicit_path, error)); return; } }; @@ -168,6 +177,7 @@ fn load_one( path: path.to_path_buf(), reason: "symbolic link".into(), error: false, + explicit: explicit_path, }); return; } @@ -175,7 +185,7 @@ fn load_one( Ok(metadata) if metadata.is_file() => metadata, Ok(_) => return, Err(error) => { - discovery.skipped.push(skip(path, error)); + discovery.skipped.push(skip(path, explicit_path, error)); return; } } @@ -188,13 +198,14 @@ fn load_one( path: path.to_path_buf(), reason: format!("larger than {} bytes", resolved.config.files.max_size), error: false, + explicit: explicit_path, }); return; } let source = match fs::read(path) { Ok(value) => value, Err(error) => { - discovery.skipped.push(skip(path, error)); + discovery.skipped.push(skip(path, explicit_path, error)); return; } }; @@ -203,6 +214,7 @@ fn load_one( path: path.to_path_buf(), reason: "binary file (NUL byte)".into(), error: false, + explicit: explicit_path, }); return; } @@ -227,6 +239,7 @@ fn load_one( path: path.to_path_buf(), reason: "language disabled by configuration".into(), error: false, + explicit: explicit_path, }); return; } @@ -252,6 +265,7 @@ fn load_one( path: path.to_path_buf(), reason: "unknown language".into(), error: false, + explicit: explicit_path, }); return; } @@ -299,10 +313,11 @@ fn compile_globs(patterns: &[String]) -> Result { builder.build().context("cannot compile file globs") } -fn skip(path: &Path, error: impl std::fmt::Display) -> SkippedFile { +fn skip(path: &Path, explicit: bool, error: impl std::fmt::Display) -> SkippedFile { SkippedFile { path: path.to_path_buf(), reason: error.to_string(), error: true, + explicit, } } diff --git a/rust/ocomment/src/git.rs b/rust/ocomment/src/git.rs index ec980e2..f06831c 100644 --- a/rust/ocomment/src/git.rs +++ b/rust/ocomment/src/git.rs @@ -1,7 +1,9 @@ use crate::{ atomic::{WritePlan, apply_transaction}, config::ResolvedConfig, - output::{self, Operation, OutputFormat, Presentation, ProcessedFile}, + output::{ + self, Operation, OutputFormat, Presentation, ProcessedFile, RenderOptions, Verbosity, + }, plugin::PluginHost, }; use anyhow::{Context, Result, anyhow, bail}; @@ -35,6 +37,8 @@ pub struct StagedRequest<'a> { pub forced_language: Option, pub forced_dialect: Option, pub presentation: Presentation, + pub verbosity: Verbosity, + pub preview: bool, } pub fn run_staged(request: StagedRequest<'_>) -> Result { @@ -48,6 +52,8 @@ pub fn run_staged(request: StagedRequest<'_>) -> Result { forced_language, forced_dialect, presentation, + verbosity, + preview, } = request; let root = repository_root()?; let names = staged_paths(&root, paths)?; @@ -173,12 +179,26 @@ pub fn run_staged(request: StagedRequest<'_>) -> Result { .iter() .any(|diagnostic| diagnostic.code == "staged-existing-block-comment") }); - if operation == Operation::Fix - && (!invalid || (resolved.config.policy.force_invalid && !staged_conflict)) - { + let applied = operation == Operation::Fix + && (!invalid || (resolved.config.policy.force_invalid && !staged_conflict)); + if applied { fix_index(&root, &entries, index_only)?; } - output::render(&files, &[], format, operation, presentation)?; + output::render( + &files, + &[], + &RenderOptions { + format, + operation, + presentation, + verbosity, + preview, + explain: false, + dry_run: false, + force_invalid: resolved.config.policy.force_invalid, + applied, + }, + )?; if invalid { return Ok(2); } diff --git a/rust/ocomment/src/lsp.rs b/rust/ocomment/src/lsp.rs index d004c47..1908ec4 100644 --- a/rust/ocomment/src/lsp.rs +++ b/rust/ocomment/src/lsp.rs @@ -1,6 +1,7 @@ use crate::{ config::{self, ResolvedConfig}, files, + output::{kept_label, removable_label}, plugin::PluginHost, }; use anyhow::Result as AnyResult; @@ -155,7 +156,7 @@ impl Backend { code: Some(NumberOrString::String("removable-comment".into())), code_description: None, source: Some("ocomment".into()), - message: format!("removable {:?} comment", comment.kind), + message: removable_label(comment.kind), related_information: None, tags: Some(vec![DiagnosticTag::UNNECESSARY]), data: None, @@ -855,11 +856,10 @@ impl LanguageServer for Backend { return Ok(None); }; let text = match &comment.disposition { - Disposition::Remove => format!("OComment: removable {:?} comment", comment.kind), - Disposition::Keep { reason } => format!( - "OComment protects this {:?} comment: {reason}", - comment.kind - ), + Disposition::Remove => format!("OComment: {}", removable_label(comment.kind)), + Disposition::Keep { reason } => { + format!("OComment: {}", kept_label(comment.kind, reason)) + } }; Ok(Some(Hover { contents: HoverContents::Scalar(MarkedString::String(text)), diff --git a/rust/ocomment/src/main.rs b/rust/ocomment/src/main.rs index ad05a9b..ac24b28 100644 --- a/rust/ocomment/src/main.rs +++ b/rust/ocomment/src/main.rs @@ -6,6 +6,7 @@ mod git; mod lsp; mod output; mod plugin; +mod values; use std::process::ExitCode; diff --git a/rust/ocomment/src/output.rs b/rust/ocomment/src/output.rs index eea9c53..0fb99e5 100644 --- a/rust/ocomment/src/output.rs +++ b/rust/ocomment/src/output.rs @@ -1,14 +1,16 @@ use crate::files::SkippedFile; use anyhow::Result; use clap::ValueEnum; -use ocomment_core::{ByteSpan, Language, TransformResult}; +use ocomment_core::{ByteSpan, CommentKind, Language, TransformResult}; use serde::Serialize; use serde_json::{Value, json}; use similar::{ChangeTag, TextDiff}; use std::{ + collections::BTreeMap, io::{self, Write}, path::{Path, PathBuf}, }; +use unicode_width::UnicodeWidthChar; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)] pub enum OutputFormat { @@ -32,7 +34,126 @@ pub enum Operation { pub struct Presentation { pub color: bool, pub hyperlinks: bool, - pub progress: bool, +} + +/// How much of the human report a run is allowed to write. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum Verbosity { + /// Only errors and diagnostics. + Quiet, + #[default] + Normal, + /// Everything, including the per-kind breakdown and every skipped file. + Verbose, +} + +/// Everything the renderer needs besides the results themselves. +#[derive(Clone, Copy, Debug)] +pub struct RenderOptions { + pub format: OutputFormat, + pub operation: Operation, + pub presentation: Presentation, + pub verbosity: Verbosity, + /// Human lines carry a one-line rendering of the comment text. + pub preview: bool, + // Plumbed for the presentation work that follows; nothing reads them yet. + #[allow(dead_code)] + pub explain: bool, + #[allow(dead_code)] + pub dry_run: bool, + /// `--force-invalid` was in effect, so a file that fails to scan still had + /// its provably safe edits applied. + pub force_invalid: bool, + /// The run reached the disk. A `fix` blocked by invalid syntax or an I/O + /// error leaves this false and must not claim any removal. + pub applied: bool, +} + +/// What one run found, counted once for the end-of-run summary. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Summary { + pub files_scanned: usize, + pub files_with_removable: usize, + pub removable_comments: usize, + pub kept_comments: usize, + pub files_changed: usize, + pub comments_removed: usize, + pub invalid_files: usize, + /// Non-error skips, counted under a short stable label rather than the + /// raw reason, which can carry a configured byte limit. + pub skipped_by_reason: BTreeMap, + pub io_errors: usize, +} + +impl Summary { + pub fn compute(files: &[ProcessedFile], skipped: &[SkippedFile], operation: Operation) -> Self { + let mut summary = Self { + files_scanned: files.len(), + ..Self::default() + }; + for file in files { + let removable = removable_count(file); + summary.removable_comments += removable; + summary.kept_comments += file.result.report.comments.len() - removable; + if removable > 0 { + summary.files_with_removable += 1; + } + if !file.result.report.valid { + summary.invalid_files += 1; + } + if file.source != file.result.output { + summary.files_changed += 1; + if operation == Operation::Fix { + summary.comments_removed += removable; + } + } + } + for item in skipped { + if item.error { + summary.io_errors += 1; + } else { + *summary + .skipped_by_reason + .entry(skip_label(&item.reason).to_owned()) + .or_default() += 1; + } + } + summary + } + + fn skipped_files(&self) -> usize { + self.skipped_by_reason.values().sum() + } +} + +fn removable_count(file: &ProcessedFile) -> usize { + file.result + .report + .comments + .iter() + .filter(|comment| comment.disposition.is_remove()) + .count() +} + +/// Fold a skip reason onto a short label the summary can group by. +fn skip_label(reason: &str) -> &str { + if reason.starts_with("larger than ") { + "too large" + } else if reason.starts_with("binary file") { + "binary" + } else if reason.starts_with("language disabled") { + "language disabled" + } else { + reason + } +} + +/// `1 comment` / `2 removable comments`: the noun is pluralized and an +/// optional adjective is placed in front of it. +fn comments(count: usize, adjective: &str) -> String { + let plural = if count == 1 { "" } else { "s" }; + let space = if adjective.is_empty() { "" } else { " " }; + format!("{count} {adjective}{space}comment{plural}") } #[derive(Clone, Debug)] @@ -53,15 +174,103 @@ struct JsonFile<'a> { source_map: &'a ocomment_core::SourceMap, } +/// The one-line label for a comment OComment would delete. +pub fn removable_label(kind: CommentKind) -> String { + format!("removable {kind} comment") +} + +/// The one-line label for a comment OComment deliberately protects. +pub fn kept_label(kind: CommentKind, reason: &str) -> String { + format!("kept {kind} comment: {reason}") +} + +/// How many display columns a comment preview may occupy. +const PREVIEW_COLUMNS: usize = 72; + +/// A one-line, terminal-safe rendering of the comment at `span`. +/// +/// Comment text is untrusted input that is about to be written to a terminal, +/// so the whole comment is folded onto one line, every control character — +/// `ESC` above all — is replaced with U+FFFD instead of being forwarded, and +/// the result is cut to `max_columns` display columns. +fn preview(source: &[u8], span: ByteSpan, max_columns: usize) -> String { + let start = span.start.min(source.len()); + let end = span.end.clamp(start, source.len()); + let text = String::from_utf8_lossy(&source[start..end]); + let mut folded = String::with_capacity(text.len()); + let mut pending_space = false; + for character in text.chars() { + if matches!(character, ' ' | '\t' | '\r' | '\n' | '\u{c}') { + // Leading whitespace is dropped, and a run only becomes a space + // once something else follows it, so the tail is trimmed too. + pending_space = !folded.is_empty(); + continue; + } + if pending_space { + folded.push(' '); + pending_space = false; + } + folded.push(if is_control(character) { + '\u{fffd}' + } else { + character + }); + } + truncate(folded, max_columns) +} + +/// C0, DEL, and C1. None of these may reach the terminal verbatim. +fn is_control(character: char) -> bool { + matches!(character, '\u{0}'..='\u{1f}' | '\u{7f}'..='\u{9f}') +} + +fn columns(character: char) -> usize { + UnicodeWidthChar::width(character).unwrap_or(0) +} + +/// Cut `text` to `max_columns` display columns, never inside a wide character, +/// leaving room for the ellipsis that marks the cut. +fn truncate(text: String, max_columns: usize) -> String { + if text.chars().map(columns).sum::() <= max_columns { + return text; + } + let budget = max_columns.saturating_sub(1); + let mut cut = String::with_capacity(text.len()); + let mut width = 0usize; + for character in text.chars() { + width += columns(character); + if width > budget { + break; + } + cut.push(character); + } + cut.push('\u{2026}'); + cut +} + +/// The `: ` tail a human line carries, dimmed when colour is on. +fn preview_suffix(source: &[u8], span: ByteSpan, options: &RenderOptions) -> String { + if !options.preview { + return String::new(); + } + let text = preview(source, span, PREVIEW_COLUMNS); + if text.is_empty() { + return String::new(); + } + format!( + ": {}{text}{}", + color("\x1b[2m", options.presentation.color), + color("\x1b[0m", options.presentation.color) + ) +} + pub fn render( files: &[ProcessedFile], skipped: &[SkippedFile], - format: OutputFormat, - operation: Operation, - presentation: Presentation, + options: &RenderOptions, ) -> Result<()> { - match format { - OutputFormat::Human => render_human(files, skipped, operation, presentation), + match options.format { + OutputFormat::Human => render_human(files, skipped, options), OutputFormat::Json => render_json(files, skipped), OutputFormat::Jsonl => render_jsonl(files, skipped), OutputFormat::Sarif => render_sarif(files, skipped), @@ -72,25 +281,30 @@ pub fn render( fn render_human( files: &[ProcessedFile], skipped: &[SkippedFile], - operation: Operation, - presentation: Presentation, + options: &RenderOptions, ) -> Result<()> { + let operation = options.operation; + let presentation = options.presentation; + let quiet = options.verbosity == Verbosity::Quiet; + let verbose = options.verbosity == Verbosity::Verbose; let stdout = io::stdout(); let mut output = stdout.lock(); for file in files { if operation == Operation::Diff && file.source != file.result.output { - write!( - output, - "{}", - unified_diff(&file.path, &file.source, &file.result.output) - )?; + if !quiet { + write!( + output, + "{}", + unified_diff(&file.path, &file.source, &file.result.output) + )?; + } continue; } for diagnostic in &file.result.report.diagnostics { let (line, column) = line_column(&file.source, diagnostic.span.start); writeln!( output, - "{}:{line}:{column}: {}{:?}[{}]{}: {}", + "{}:{line}:{column}: {}{}[{}]{}: {}", display_path(&file.path, presentation.hyperlinks), color("\x1b[31m", presentation.color), diagnostic.severity, @@ -99,20 +313,33 @@ fn render_human( diagnostic.message )?; } + if quiet { + continue; + } if operation == Operation::Scan { for comment in &file.result.report.comments { let (line, column) = line_column(&file.source, comment.span.start); writeln!( output, - "{}:{line}:{column}: {:?} {:?} {}..{}", + "{}:{line}:{column}: {} {} {}..{}{}", display_path(&file.path, presentation.hyperlinks), comment.kind, comment.disposition, comment.span.start, - comment.span.end + comment.span.end, + preview_suffix(&file.source, comment.span, options) + )?; + } + } else if operation == Operation::Fix { + if options.applied && file.source != file.result.output { + writeln!( + output, + "fixed {}: removed {}", + display_path(&file.path, presentation.hyperlinks), + comments(removable_count(file), "") )?; } - } else if operation != Operation::Fix { + } else { for comment in file .result .report @@ -123,17 +350,21 @@ fn render_human( let (line, column) = line_column(&file.source, comment.span.start); writeln!( output, - "{}:{line}:{column}: {}removable {:?} comment{}", + "{}:{line}:{column}: {}{}{}{}", display_path(&file.path, presentation.hyperlinks), color("\x1b[33m", presentation.color), - comment.kind, - color("\x1b[0m", presentation.color) + removable_label(comment.kind), + color("\x1b[0m", presentation.color), + preview_suffix(&file.source, comment.span, options) )?; } } } if operation != Operation::Diff { for item in skipped { + if !item.error && (quiet || !(item.explicit || verbose)) { + continue; + } writeln!( output, "{}: {}: {}", @@ -143,16 +374,128 @@ fn render_human( )?; } } - if presentation.progress { - eprintln!( - "ocomment: processed {} file(s), skipped {}", - files.len(), - skipped.len() - ); + if quiet { + return Ok(()); + } + let summary = Summary::compute(files, skipped, operation); + let folded = !verbose && skipped.iter().any(|item| !item.error && !item.explicit); + let stderr = io::stderr(); + let mut report = stderr.lock(); + if verbose && let Some(line) = kind_breakdown(files, options) { + writeln!(report, "{line}")?; + } + writeln!( + report, + "{}{}", + summary_line(&summary, options), + skip_clause(&summary, folded) + )?; + if summary.invalid_files > 0 && !options.force_invalid { + writeln!( + report, + "{} file(s) have invalid syntax; nothing was written for them \ + (use --force-invalid to apply known-safe edits).", + summary.invalid_files + )?; } Ok(()) } +/// The one-line verdict for the run, without the skipped-file clause. +fn summary_line(summary: &Summary, options: &RenderOptions) -> String { + let scanned = summary.files_scanned; + let found = || { + format!( + "Found {} in {} file(s) ({scanned} files scanned).", + comments(summary.removable_comments, "removable"), + summary.files_with_removable + ) + }; + match options.operation { + Operation::Check | Operation::Diff => { + if summary.removable_comments == 0 { + return format!("No removable comments in {scanned} file(s)."); + } + let next = if options.operation == Operation::Diff { + "apply" + } else { + "remove them" + }; + format!("{} Run `ocomment fix` to {next}.", found()) + } + Operation::Fix => { + if options.applied && summary.files_changed > 0 { + format!( + "Removed {} in {} file(s) ({scanned} files scanned).", + comments(summary.comments_removed, ""), + summary.files_changed + ) + } else if summary.removable_comments == 0 { + format!("Nothing to fix in {scanned} file(s).") + } else { + // The transaction never reached the disk; report what is still + // there rather than claiming a removal. + found() + } + } + Operation::Scan => format!( + "Scanned {scanned} file(s): {} ({} removable, {} kept).", + comments(summary.removable_comments + summary.kept_comments, ""), + summary.removable_comments, + summary.kept_comments + ), + } +} + +/// The skipped-file clause appended to the summary line. +fn skip_clause(summary: &Summary, folded: bool) -> String { + let total = summary.skipped_files(); + if total == 0 { + return String::new(); + } + let reasons: Vec<_> = summary + .skipped_by_reason + .iter() + .map(|(label, count)| format!("{label}: {count}")) + .collect(); + let hint = if folded { "; use -v to list" } else { "" }; + format!(" {total} file(s) skipped ({}{hint}).", reasons.join(", ")) +} + +/// The `-v` breakdown of what each comment kind contributed. +fn kind_breakdown(files: &[ProcessedFile], options: &RenderOptions) -> Option { + let verb = if options.operation == Operation::Fix && options.applied { + "removed" + } else { + "removable" + }; + let mut removable = [0usize; CommentKind::ALL.len()]; + let mut kept = [0usize; CommentKind::ALL.len()]; + for file in files { + for comment in &file.result.report.comments { + let slot = CommentKind::ALL + .iter() + .position(|kind| *kind == comment.kind) + .expect("CommentKind::ALL lists every kind"); + if comment.disposition.is_remove() { + removable[slot] += 1; + } else { + kept[slot] += 1; + } + } + } + let mut parts = Vec::new(); + for (slot, kind) in CommentKind::ALL.into_iter().enumerate() { + if removable[slot] > 0 { + parts.push(format!("{kind} {} {verb}", removable[slot])); + } + if kept[slot] > 0 { + parts.push(format!("{kind} {} kept", kept[slot])); + } + } + (!parts.is_empty()).then(|| format!("kinds: {}", parts.join(", "))) +} + fn color(code: &'static str, enabled: bool) -> &'static str { if enabled { code } else { "" } } @@ -227,14 +570,11 @@ fn render_sarif(files: &[ProcessedFile], skipped: &[SkippedFile]) -> Result<()> { let (line, column) = line_column(&file.source, comment.span.start); let (end_line, end_column) = line_column(&file.source, comment.span.end); - let kind = serde_json::to_value(comment.kind)? - .as_str() - .unwrap_or("comment") - .to_owned(); + let kind = comment.kind.as_str(); results.push(json!({ "ruleId": format!("removable-{kind}"), "level": "note", - "message": {"text": format!("removable {:?} comment", comment.kind)}, + "message": {"text": removable_label(comment.kind)}, "locations": [{"physicalLocation": { "artifactLocation": {"uri": file.path.to_string_lossy()}, "region": {"startLine": line, "startColumn": column, @@ -312,9 +652,9 @@ fn render_github(files: &[ProcessedFile], skipped: &[SkippedFile]) -> Result<()> { let (line, column) = line_column(&file.source, comment.span.start); println!( - "::notice file={},line={line},col={column}::removable {:?} comment", + "::notice file={},line={line},col={column}::{}", github_escape(&file.path.to_string_lossy()), - comment.kind + removable_label(comment.kind) ); } for diagnostic in &file.result.report.diagnostics { @@ -419,3 +759,59 @@ pub fn invalid(files: &[ProcessedFile]) -> bool { fn _span(_: ByteSpan) -> Value { Value::Null } + +#[cfg(test)] +mod tests { + use super::*; + + fn preview_of(source: &[u8], max_columns: usize) -> String { + preview(source, ByteSpan::new(0, source.len()), max_columns) + } + + #[test] + fn preview_collapses_every_run_of_whitespace_and_trims() { + assert_eq!( + preview_of(b" /*\r\n\tkeep\t this tidy \x0c*/ ", 72), + "/* keep this tidy */" + ); + } + + #[test] + fn preview_truncates_on_display_width_without_splitting_a_wide_character() { + let source = "ab漢字漢字漢字ab".as_bytes(); + assert_eq!(preview_of(source, 20), "ab漢字漢字漢字ab"); + let cut = preview_of(source, 10); + assert_eq!(cut, "ab漢字漢…"); + assert!(cut.ends_with('…'), "truncation is unmarked: {cut}"); + let width: usize = cut + .chars() + .map(|ch| unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0)) + .sum(); + assert!(width <= 10, "`{cut}` is {width} columns wide"); + } + + #[test] + fn preview_replaces_control_characters_with_the_replacement_character() { + let source = b"// \x1b[31m\x07 \xc2\x9b\x7f bell"; + let rendered = preview_of(source, 72); + assert_eq!(rendered, "// \u{fffd}[31m\u{fffd} \u{fffd}\u{fffd} bell"); + assert!( + !rendered.contains('\x1b'), + "an escape sequence survived: {rendered:?}" + ); + } + + #[test] + fn preview_replaces_invalid_utf8_bytes() { + assert_eq!( + preview_of(b"// \xff\xfe end", 72), + "// \u{fffd}\u{fffd} end" + ); + } + + #[test] + fn preview_reads_only_the_span() { + let source = b"let x = 1; // TODO remove\n"; + assert_eq!(preview(source, ByteSpan::new(11, 25), 72), "// TODO remove"); + } +} diff --git a/rust/ocomment/src/values.rs b/rust/ocomment/src/values.rs new file mode 100644 index 0000000..dad3557 --- /dev/null +++ b/rust/ocomment/src/values.rs @@ -0,0 +1,239 @@ +//! `clap::ValueEnum` wrappers around the core vocabulary enums. +//! +//! The orphan rule forbids implementing `clap::ValueEnum` on the types owned by +//! `ocomment-core`, so every user-facing enum gets a transparent newtype here. +//! Names, aliases, and the variant list all come from the core enum, which stays +//! the single source of truth; this module only adds the per-value help clap +//! needs for `--help`, error messages, and shell completions. + +use clap::{ValueEnum, builder::PossibleValue}; +use ocomment_core::{CommentKind, Dialect, Language, Layout, Policy}; +use std::ops::Deref; + +/// Register the canonical spelling plus every accepted alias. +/// +/// `clap` matches a command-line value through `PossibleValue::matches`, so an +/// alias that is not registered here is not accepted, however well the core +/// `FromStr` understands it. Core aliases are stored with `-` as the separator; +/// the `_` spelling is registered too so both keep working. +fn possible_value( + name: &'static str, + aliases: &'static [&'static str], + help: &'static str, +) -> PossibleValue { + let mut value = PossibleValue::new(name).help(help); + for alias in aliases { + value = value.alias(*alias); + } + for spelling in std::iter::once(&name).chain(aliases) { + if spelling.contains('-') { + value = value.alias(spelling.replace('-', "_")); + } + } + value +} + +macro_rules! value_enum_wrapper { + ($name:ident, $inner:ty, $help:expr) => { + #[doc = concat!("A `clap::ValueEnum` view of [`", stringify!($inner), "`].")] + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct $name(pub $inner); + + impl From<$name> for $inner { + fn from(value: $name) -> Self { + value.0 + } + } + + impl From<$inner> for $name { + fn from(value: $inner) -> Self { + Self(value) + } + } + + impl Deref for $name { + type Target = $inner; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl ValueEnum for $name { + fn value_variants<'a>() -> &'a [Self] { + static VARIANTS: [$name; <$inner>::ALL.len()] = { + let mut variants = [$name(<$inner>::ALL[0]); <$inner>::ALL.len()]; + let mut index = 0; + while index < variants.len() { + variants[index] = $name(<$inner>::ALL[index]); + index += 1; + } + variants + }; + &VARIANTS + } + + fn to_possible_value(&self) -> Option { + let help: fn($inner) -> &'static str = $help; + Some(possible_value( + self.0.as_str(), + self.0.aliases(), + help(self.0), + )) + } + } + }; +} + +value_enum_wrapper!(PolicyArg, Policy, |value| match value { + Policy::Safe => "Remove ordinary and doc comments; keep preambles and directives", + Policy::Legal => "Like safe, and keep licence and copyright comments as well", + Policy::All => "Remove every comment that no keep override protects", +}); + +value_enum_wrapper!(LayoutArg, Layout, |value| match value { + Layout::Lines => "Keep the line structure and separate tokens that would otherwise join", + Layout::Columns => "Pad each removed comment so the following columns do not shift", + Layout::Compact => "Drop the comment bytes and keep only the newlines it spanned", +}); + +// The CLI is deliberately stricter than the core `FromStr`, which folds case, +// dashes, and underscores away before it looks a name up: only the canonical +// spelling, the pinned aliases, and their underscore variants are registered +// here, so `--language r-u-s-t` stays an error even though the core accepts it. +value_enum_wrapper!(LanguageArg, Language, |value| match value { + Language::Rust => "Rust source files", + Language::Ocaml => "OCaml implementation and interface files", + Language::C => "C source and header files", + Language::Cpp => "C++ source and header files", + Language::Go => "Go source files", + Language::Java => "Java source files, including Unicode escape translation", + Language::JavaScript => "JavaScript modules and scripts, including JSX", + Language::TypeScript => "TypeScript modules and scripts, including TSX", + Language::Python => "Python source and stub files", + Language::Shell => "POSIX sh, Bash, and zsh scripts", + Language::Html => "HTML documents, including nested script and style elements", + Language::Css => "CSS stylesheets", + Language::Jsonc => "JSON with comments, including JSON5", + Language::Sql => "SQL for every supported database dialect", + Language::Kotlin => "Kotlin source and script files", + Language::Unknown => "An undetected language", +}); + +value_enum_wrapper!(DialectArg, Dialect, |value| match value { + Dialect::Standard => "The default lexical rules of the language", + Dialect::Jsx => "JavaScript with JSX elements", + Dialect::Tsx => "TypeScript with JSX elements", + Dialect::ObjectiveC => "Objective-C extensions to C", + Dialect::ObjectiveCpp => "Objective-C++ extensions to C++", + Dialect::GnuC => "GNU extensions to C", + Dialect::GnuCpp => "GNU extensions to C++", + Dialect::Cuda => "CUDA extensions to C++", + Dialect::PosixSh => "The POSIX shell command language", + Dialect::Bash53 => "Bash 5.3", + Dialect::Zsh => "The Z shell", + Dialect::PostgreSql => "PostgreSQL, with dollar-quoted bodies", + Dialect::MySql => "MySQL, including its executable versioned comments", + Dialect::Sqlite => "SQLite", + Dialect::TSql => "Microsoft Transact-SQL", + Dialect::Oracle => "Oracle SQL and PL/SQL", +}); + +value_enum_wrapper!(CommentKindArg, CommentKind, |value| match value { + CommentKind::Line => "An ordinary comment running to the end of the line", + CommentKind::Block => "An ordinary delimited comment", + CommentKind::DocLine => "A documentation comment running to the end of the line", + CommentKind::DocBlock => "A delimited documentation comment", + CommentKind::Directive => "A tool or language directive such as a pragma or lint control", + CommentKind::License => "A licence or copyright preamble", + CommentKind::HtmlComment => "A DOM-observable HTML comment", + CommentKind::Shebang => "The interpreter line starting an executable script", + CommentKind::Encoding => "A source encoding declaration", + CommentKind::OptimizerHint => "A compiler or database optimizer hint", + CommentKind::VersionComment => "A MySQL versioned comment that the server executes", +}); + +#[cfg(test)] +mod tests { + use super::*; + + /// Every spelling the core enum accepts must reach clap, which matches only + /// through the registered name and aliases. + fn round_trip(canonical: &'static str, aliases: &'static [&'static str]) + where + T: ValueEnum + Copy + PartialEq + std::fmt::Debug, + { + let expected = T::from_str(canonical, false) + .unwrap_or_else(|_| panic!("clap rejects the canonical name `{canonical}`")); + for spelling in std::iter::once(&canonical).chain(aliases) { + for candidate in [ + (*spelling).to_owned(), + spelling.to_ascii_uppercase(), + spelling.replace('-', "_"), + ] { + let parsed = T::from_str(&candidate, true) + .unwrap_or_else(|_| panic!("clap rejects `{candidate}`")); + assert_eq!( + parsed, expected, + "`{candidate}` resolved to the wrong value" + ); + } + } + } + + #[test] + fn every_core_spelling_reaches_clap() { + for value in Policy::ALL { + round_trip::(value.as_str(), value.aliases()); + } + for value in Layout::ALL { + round_trip::(value.as_str(), value.aliases()); + } + for value in Language::ALL { + round_trip::(value.as_str(), value.aliases()); + } + for value in Dialect::ALL { + round_trip::(value.as_str(), value.aliases()); + } + for value in CommentKind::ALL { + round_trip::(value.as_str(), value.aliases()); + } + } + + #[test] + fn every_value_carries_help_and_the_core_variant_order() { + assert_eq!( + LanguageArg::value_variants() + .iter() + .map(|value| value.0) + .collect::>(), + Language::ALL.to_vec() + ); + for value in DialectArg::value_variants() { + let possible = value + .to_possible_value() + .expect("dialects are never hidden"); + assert_eq!(possible.get_name(), value.0.as_str()); + assert!( + possible.get_help().is_some(), + "`{}` has no help text", + value.0 + ); + } + } + + #[test] + fn punctuated_aliases_survive() { + assert_eq!( + LanguageArg::from_str("c++", false), + Ok(LanguageArg(Language::Cpp)) + ); + assert_eq!( + DialectArg::from_str("objective-c++", false), + Ok(DialectArg(Dialect::ObjectiveCpp)) + ); + assert_eq!( + DialectArg::from_str("bash-5.3", false), + Ok(DialectArg(Dialect::Bash53)) + ); + } +} diff --git a/rust/ocomment/tests/cli.rs b/rust/ocomment/tests/cli.rs index 18acd93..4f5a138 100644 --- a/rust/ocomment/tests/cli.rs +++ b/rust/ocomment/tests/cli.rs @@ -458,3 +458,899 @@ fn symlink_following_is_explicitly_configurable() { assert_eq!(followed.status.code(), Some(1)); assert!(String::from_utf8_lossy(&followed.stdout).contains("removable")); } + +fn subcommand_lines(help: &str) -> Vec<(String, String)> { + let mut lines = help.lines().skip_while(|line| *line != "Commands:"); + lines.next(); + lines + .take_while(|line| !line.trim().is_empty()) + .map(|line| { + let trimmed = line.trim_start(); + match trimmed.split_once(" ") { + Some((name, description)) => (name.to_owned(), description.trim().to_owned()), + None => (trimmed.to_owned(), String::new()), + } + }) + .collect() +} + +#[test] +fn help_describes_every_subcommand() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["--help"]); + assert_eq!(output.status.code(), Some(0)); + let help = String::from_utf8(output.stdout).unwrap(); + let listed = subcommand_lines(&help); + assert!(!listed.is_empty(), "no Commands section in:\n{help}"); + for (name, description) in &listed { + assert!( + !description.is_empty(), + "subcommand `{name}` has no description in:\n{help}" + ); + } + let expected = [ + ("check", "Report removable comments (default command)"), + ( + "fix", + "Remove comments in place through an atomic, rollback-backed transaction", + ), + ("diff", "Print a unified diff of the changes fix would make"), + ( + "scan", + "List every comment with its kind, disposition and byte span", + ), + ( + "strip", + "Read source on stdin and write the stripped result to stdout", + ), + ("lsp", "Run the LSP 3.18 server over stdio"), + ( + "init", + "Write a starter .ocomment.toml or Lefthook configuration", + ), + ( + "config", + "Show, locate, explain, or export the resolved configuration", + ), + ( + "languages", + "List built-in languages, extensions, and dialects", + ), + ("plugin", "Manage sandboxed WASM scanner plugins"), + ("completions", "Generate shell completions"), + ( + "doctor", + "Diagnose the environment (config, git, plugins, tools)", + ), + ("man", "Render the roff manual page to stdout"), + ]; + for (name, description) in expected { + let found = listed + .iter() + .find(|(listed_name, _)| listed_name == name) + .unwrap_or_else(|| panic!("subcommand `{name}` is missing from:\n{help}")); + assert_eq!(found.1, description, "wrong description for `{name}`"); + } + assert!( + listed.iter().any(|(name, _)| name == "man"), + "the `man` subcommand must be documented in:\n{help}" + ); +} + +#[test] +fn help_documents_exit_status_files_and_examples() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["--help"]); + assert_eq!(output.status.code(), Some(0)); + let help = String::from_utf8(output.stdout).unwrap(); + for needle in [ + "EXIT STATUS", + "FILES", + "EXAMPLES", + ".ocomment.toml", + ".ocommentignore", + ".ocomment.lock", + ] { + assert!(help.contains(needle), "`--help` lacks {needle}:\n{help}"); + } +} + +#[test] +fn check_help_groups_options_and_lists_possible_values() { + let directory = tempfile::tempdir().unwrap(); + let short = run(directory.path(), &["check", "-h"]); + assert_eq!(short.status.code(), Some(0)); + let short = String::from_utf8(short.stdout).unwrap(); + assert!( + short.contains("[possible values: safe, legal, all]"), + "`check -h` lacks the policy values:\n{short}" + ); + assert!(short.contains("Policy:"), "no Policy heading:\n{short}"); + assert!(short.contains("Output:"), "no Output heading:\n{short}"); + + let long = run(directory.path(), &["check", "--help"]); + assert_eq!(long.status.code(), Some(0)); + let long = String::from_utf8(long.stdout).unwrap(); + assert!(long.contains("Policy:"), "no Policy heading:\n{long}"); + assert!(long.contains("Output:"), "no Output heading:\n{long}"); + for needle in [ + "- safe:", + "- legal:", + "- all:", + "- lines:", + "- rust:", + "- doc-line:", + ] { + assert!( + long.contains(needle), + "`check --help` lacks documented value {needle}:\n{long}" + ); + } +} + +#[test] +fn unknown_policy_value_reports_the_possible_values() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["--policy", "foo"]); + assert_eq!(output.status.code(), Some(2)); + let error = String::from_utf8_lossy(&output.stderr); + assert!(error.contains("invalid value 'foo'"), "{error}"); + assert!( + error.contains("[possible values: safe, legal, all]"), + "{error}" + ); +} + +#[test] +fn language_aliases_are_accepted_on_the_command_line() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + for alias in ["rs", "c++", "RUST"] { + let output = run( + directory.path(), + &["check", "sample.rs", "--language", alias], + ); + let error = String::from_utf8_lossy(&output.stderr); + assert!( + !error.contains("invalid value"), + "`--language {alias}` was rejected: {error}" + ); + assert_eq!( + output.status.code(), + Some(1), + "`--language {alias}`: {error}" + ); + } +} + +#[test] +fn unsupported_dialect_names_the_supported_ones() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + let output = run( + directory.path(), + &[ + "check", + "sample.rs", + "--language", + "rust", + "--dialect", + "jsx", + ], + ); + assert_eq!(output.status.code(), Some(2)); + let error = String::from_utf8_lossy(&output.stderr); + assert!( + error.contains("unsupported dialect `jsx` for rust"), + "{error}" + ); + assert!(error.contains("supported: standard"), "{error}"); +} + +#[test] +fn man_subcommand_renders_a_roff_page() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["man"]); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let page = String::from_utf8(output.stdout).unwrap(); + // roff requires the `\*(Aq` string definition before the title macro, so + // `.TH` is the first macro that is not a string definition. + let header = page + .lines() + .find(|line| !line.starts_with(".ie ") && !line.starts_with(".el ")) + .unwrap_or_default(); + assert!( + header.starts_with(".TH"), + "man page starts with:\n{page:.120}" + ); + assert!( + header.contains("ocomment"), + "the .TH header does not name the tool" + ); + assert!(page.contains(".SH NAME"), "man page has no NAME section"); +} + +/// The shipped page had an uppercase title, the "User Commands" manual, and a +/// SEE ALSO pointer; the generated page must keep all three. +#[test] +fn man_page_keeps_the_shipped_title_manual_and_see_also() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["man"]); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let page = String::from_utf8(output.stdout).unwrap(); + for needle in [ + ".TH OCOMMENT 1", + "User Commands", + "SEE ALSO", + "The complete schemas and guides are available in the OComment repository.", + ] { + assert!(page.contains(needle), "man page lacks {needle}:\n{page}"); + } +} + +#[test] +fn bash_completions_carry_the_policy_values() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["completions", "bash"]); + assert_eq!(output.status.code(), Some(0)); + let script = String::from_utf8(output.stdout).unwrap(); + for value in ["safe", "legal", "all"] { + assert!( + script.contains(value), + "bash completions lack the policy value {value}" + ); + } +} + +/// Rust `Debug` spellings that must never reach a terminal again. Bare +/// `Remove` is checked separately: SARIF legitimately says "Remove comment +/// with OComment" in its fix description. +const DEBUG_LEAKS: [&str; 3] = ["DocBlock", "Keep {", "Shebang"]; + +fn assert_no_debug_leak(context: &str, text: &str) { + for leak in DEBUG_LEAKS { + assert!( + !text.contains(leak), + "{context} leaks the Rust Debug token `{leak}`:\n{text}" + ); + } +} + +#[test] +fn human_check_names_comment_kinds_in_canonical_spelling() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("doc.rs"), + b"/** doc */\nfn main() {}\n", + ) + .unwrap(); + let output = run(directory.path(), &["check", "doc.rs"]); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains("removable doc-block comment"), + "check output is:\n{stdout}" + ); + assert_no_debug_leak("human check output", &stdout); + assert!(!stdout.contains("Remove"), "check output is:\n{stdout}"); +} + +#[test] +fn human_scan_lines_use_canonical_kinds_and_dispositions() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("a.py"), + b"#!/usr/bin/env python3\nx = 1 # remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["scan", "a.py"]); + assert_eq!(output.status.code(), Some(0)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert_eq!( + stdout, + "a.py:1:1: shebang keep (required source preamble) 0..22: #!/usr/bin/env python3\n\ + a.py:2:8: line remove 30..38: # remove\n" + ); + assert_no_debug_leak("human scan output", &stdout); +} + +#[test] +fn human_diagnostics_lowercase_the_severity() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("broken.c"), b"int x; /* open").unwrap(); + let output = run(directory.path(), &["check", "broken.c"]); + assert_eq!(output.status.code(), Some(2)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains("error[unterminated-comment]: unterminated block comment"), + "check output is:\n{stdout}" + ); + assert!( + !stdout.contains("Error["), + "check output still Debug-prints the severity:\n{stdout}" + ); +} + +#[test] +fn config_explain_prints_canonical_policy_and_layout() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["config", "explain"]); + assert_eq!(output.status.code(), Some(0)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains("policy: safe; layout: lines"), + "config explain output is:\n{stdout}" + ); + // Only the policy line is pinned: the surrounding lines print filesystem + // paths that may legitimately contain any spelling. + let policy_line = stdout + .lines() + .find(|line| line.starts_with("policy:")) + .unwrap_or_else(|| panic!("config explain has no policy line:\n{stdout}")); + assert!( + !policy_line.contains("Safe") && !policy_line.contains("Lines"), + "config explain still Debug-prints the enums:\n{policy_line}" + ); +} + +#[test] +fn github_annotations_use_kebab_comment_kinds() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("doc.rs"), + b"/** doc */\nfn main() {}\n", + ) + .unwrap(); + let output = run(directory.path(), &["check", "doc.rs", "--format", "github"]); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert_eq!( + stdout, + "::notice file=doc.rs,line=1,col=1::removable doc-block comment\n" + ); + assert_no_debug_leak("github annotations", &stdout); + assert!(!stdout.contains("Remove"), "github output is:\n{stdout}"); +} + +#[test] +fn sarif_keeps_kebab_rule_ids_and_canonical_messages() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("doc.rs"), + b"/** doc */\nfn main() {}\n", + ) + .unwrap(); + let output = run(directory.path(), &["check", "doc.rs", "--format", "sarif"]); + assert_eq!(output.status.code(), Some(1)); + let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let result = &value["runs"][0]["results"][0]; + assert_eq!(result["ruleId"], "removable-doc-block"); + assert_eq!(result["message"]["text"], "removable doc-block comment"); + assert_no_debug_leak("SARIF report", &String::from_utf8(output.stdout).unwrap()); +} + +#[test] +fn json_and_jsonl_serde_names_are_frozen() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.py"), + b"#!/usr/bin/env python3\n# SPDX-License-Identifier: MIT\nx = 1 # remove\n", + ) + .unwrap(); + + let jsonl = run( + directory.path(), + &["scan", "sample.py", "--format", "jsonl"], + ); + assert_eq!(jsonl.status.code(), Some(0)); + assert_eq!( + String::from_utf8(jsonl.stdout).unwrap(), + concat!( + r#"{"path":"sample.py","language":"python","changed":true,"report":{"language":"python","#, + r#""comments":[{"span":{"start":0,"end":22},"kind":"shebang","disposition":{"action":"keep","#, + r#""reason":"required source preamble"}},{"span":{"start":23,"end":53},"kind":"license","#, + r#""disposition":{"action":"remove"}},{"span":{"start":61,"end":69},"kind":"line","#, + r#""disposition":{"action":"remove"}}],"diagnostics":[],"valid":true},"#, + r#""edits":[{"span":{"start":23,"end":53},"replacement":""},"#, + r#"{"span":{"start":61,"end":69},"replacement":""}],"#, + r#""source_map":{"segments":[{"original":{"start":0,"end":23},"output":{"start":0,"end":23},"exact":true},"#, + r#"{"original":{"start":23,"end":53},"output":{"start":23,"end":23},"exact":false},"#, + r#"{"original":{"start":53,"end":61},"output":{"start":23,"end":31},"exact":true},"#, + r#"{"original":{"start":61,"end":69},"output":{"start":31,"end":31},"exact":false},"#, + r#"{"original":{"start":69,"end":70},"output":{"start":31,"end":32},"exact":true}]}}"#, + "\n" + ), + "the JSONL protocol changed" + ); + + let json = run(directory.path(), &["scan", "sample.py", "--format", "json"]); + assert_eq!(json.status.code(), Some(0)); + let value: serde_json::Value = serde_json::from_slice(&json.stdout).unwrap(); + let comments = value["files"][0]["report"]["comments"].as_array().unwrap(); + assert_eq!( + comments + .iter() + .map(|comment| comment["kind"].as_str().unwrap()) + .collect::>(), + ["shebang", "license", "line"] + ); + assert_eq!(comments[0]["disposition"]["action"], "keep"); + assert_eq!( + comments[0]["disposition"]["reason"], + "required source preamble" + ); + assert_eq!(comments[1]["disposition"]["action"], "remove"); + assert_eq!(value["files"][0]["language"], "python"); +} + +#[test] +fn json_diagnostics_keep_the_lower_case_serde_severity() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("broken.c"), b"int x; /* open").unwrap(); + let output = run(directory.path(), &["scan", "broken.c", "--format", "json"]); + assert_eq!(output.status.code(), Some(2)); + let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let diagnostic = &value["files"][0]["report"]["diagnostics"][0]; + assert_eq!(diagnostic["severity"], "error"); + assert_eq!(diagnostic["code"], "unterminated-comment"); +} + +/// The end-of-run summary belongs on standard error so that `check` keeps a +/// grep-able `path:line:col` stream on standard output. +#[test] +fn check_writes_its_summary_to_standard_error() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["check", "sample.rs"]); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!( + stdout.contains("removable line comment"), + "check output is:\n{stdout}" + ); + assert!( + !stdout.contains("Found"), + "the summary leaked onto stdout:\n{stdout}" + ); + assert_eq!( + stderr, + "Found 1 removable comment in 1 file(s) (1 files scanned). \ + Run `ocomment fix` to remove them.\n" + ); +} + +#[test] +fn a_clean_check_summarizes_the_files_it_scanned() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("sample.rs"), b"let x = 1;\n").unwrap(); + let output = run(directory.path(), &["check", "sample.rs"]); + assert_eq!(output.status.code(), Some(0)); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "No removable comments in 1 file(s).\n" + ); + assert_eq!(String::from_utf8(output.stdout).unwrap(), ""); +} + +/// `diff` must keep standard output a clean patch. +#[test] +fn diff_keeps_the_patch_on_stdout_and_summarizes_on_stderr() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["diff", "sample.rs"]); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.starts_with("--- a/sample.rs"), "diff is:\n{stdout}"); + assert!( + !stdout.contains("Found"), + "the summary leaked into the patch:\n{stdout}" + ); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "Found 1 removable comment in 1 file(s) (1 files scanned). \ + Run `ocomment fix` to apply.\n" + ); +} + +#[test] +fn fix_reports_every_changed_file_and_summarizes_on_stderr() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["fix", "sample.rs"]); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + String::from_utf8(output.stdout).unwrap(), + "fixed sample.rs: removed 1 comment\n" + ); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "Removed 1 comment in 1 file(s) (1 files scanned).\n" + ); +} + +#[test] +fn a_clean_fix_says_there_was_nothing_to_do() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("sample.rs"), b"let x = 1;\n").unwrap(); + let output = run(directory.path(), &["fix", "sample.rs"]); + assert_eq!(output.status.code(), Some(0)); + assert_eq!(String::from_utf8(output.stdout).unwrap(), ""); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "Nothing to fix in 1 file(s).\n" + ); +} + +#[test] +fn scan_summarizes_the_comment_counts() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("a.py"), + b"#!/usr/bin/env python3\nx = 1 # remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["scan", "a.py"]); + assert_eq!(output.status.code(), Some(0)); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "Scanned 1 file(s): 2 comments (1 removable, 1 kept).\n" + ); +} + +#[test] +fn quiet_silences_a_check_that_still_exits_one() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["check", "-q", "sample.rs"]); + assert_eq!(output.status.code(), Some(1)); + assert_eq!(String::from_utf8(output.stdout).unwrap(), ""); + assert_eq!(String::from_utf8(output.stderr).unwrap(), ""); +} + +#[test] +fn quiet_and_verbose_cannot_be_combined() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["check", "-q", "-v"]); + assert_eq!(output.status.code(), Some(2)); + assert!( + String::from_utf8_lossy(&output.stderr).contains("cannot be used with"), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn verbose_traces_the_root_target_config_and_kinds() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["check", "-v", "sample.rs"]); + assert_eq!(output.status.code(), Some(1)); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!(stderr.contains("root: "), "verbose trace is:\n{stderr}"); + assert!( + stderr.contains("target: sample.rs"), + "verbose trace is:\n{stderr}" + ); + assert!(stderr.contains("config: "), "verbose trace is:\n{stderr}"); + assert!( + stderr.contains("kinds: line 1 removable"), + "verbose trace is:\n{stderr}" + ); +} + +#[test] +fn directory_walks_fold_skipped_files_into_the_summary() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + fs::write(directory.path().join("notes.md"), b"# notes\n").unwrap(); + let output = run(directory.path(), &["check", "."]); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!( + !stdout.contains("notes.md"), + "a walked skip was listed individually:\n{stdout}" + ); + assert!( + stderr.contains("1 file(s) skipped (unknown language: 1; use -v to list)."), + "summary is:\n{stderr}" + ); +} + +#[test] +fn verbose_lists_the_folded_skips() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + fs::write(directory.path().join("notes.md"), b"# notes\n").unwrap(); + let output = run(directory.path(), &["check", "-v", "."]); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains("notes.md: skipped: unknown language"), + "verbose check output is:\n{stdout}" + ); +} + +#[test] +fn an_explicit_unknown_language_argument_is_still_listed() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("notes.md"), b"# notes\n").unwrap(); + let output = run(directory.path(), &["check", "notes.md"]); + assert_eq!(output.status.code(), Some(0)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains("notes.md: skipped: unknown language"), + "check output is:\n{stdout}" + ); +} + +#[test] +fn machine_formats_never_emit_the_summary() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + for format in ["json", "jsonl", "sarif", "github"] { + let output = run( + directory.path(), + &["check", "sample.rs", "--format", format], + ); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "", + "the {format} format wrote to standard error" + ); + } +} + +/// A single invalid file blocks the whole transaction, so the summary must not +/// claim removals that never reached the disk. +#[test] +fn a_blocked_fix_does_not_claim_removals() { + let directory = tempfile::tempdir().unwrap(); + let good = directory.path().join("good.rs"); + let original = b"let x = 1; // remove\n"; + fs::write(&good, original).unwrap(); + fs::write(directory.path().join("broken.c"), b"int x; /* open").unwrap(); + let output = run(directory.path(), &["fix", "."]); + assert_eq!(output.status.code(), Some(2)); + assert_eq!(fs::read(&good).unwrap(), original); + let stdout = String::from_utf8(output.stdout).unwrap(); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!( + !stdout.contains("fixed "), + "fix claimed a write that was blocked:\n{stdout}" + ); + assert!( + stderr.contains("1 file(s) have invalid syntax; nothing was written for them (use --force-invalid to apply known-safe edits)."), + "summary is:\n{stderr}" + ); + assert!( + !stderr.contains("Removed "), + "summary claims removals that were blocked:\n{stderr}" + ); +} + +#[test] +fn staged_runs_also_emit_the_summary() { + let directory = repository(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + git(directory.path(), &["add", "sample.rs"]); + let output = run(directory.path(), &["check", "--staged"]); + assert_eq!(output.status.code(), Some(1)); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "Found 1 removable comment in 1 file(s) (1 files scanned). \ + Run `ocomment fix` to remove them.\n" + ); +} + +#[test] +fn help_lists_the_verbosity_flags_and_retires_the_progress_indicator() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["check", "--help"]); + assert_eq!(output.status.code(), Some(0)); + let help = String::from_utf8(output.stdout).unwrap(); + for needle in ["-q, --quiet", "-v, --verbose", "--progress "] { + assert!( + help.contains(needle), + "`check --help` lacks {needle}:\n{help}" + ); + } + assert!( + !help.contains("progress indicator"), + "`--progress` still promises an indicator that is never drawn:\n{help}" + ); +} + +/// `--progress` stays accepted, but the summary replaced the line it drew. +#[test] +fn the_progress_flag_no_longer_prints_a_progress_line() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + let output = run( + directory.path(), + &["check", "--progress", "always", "sample.rs"], + ); + assert_eq!(output.status.code(), Some(1)); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "Found 1 removable comment in 1 file(s) (1 files scanned). \ + Run `ocomment fix` to remove them.\n" + ); +} + +/// The human report says what the comment is, not merely that one is there. +#[test] +fn check_previews_the_comment_text_on_the_reported_line() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // TODO remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["check", "sample.rs"]); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains(": removable line comment: // TODO remove"), + "check output is:\n{stdout}" + ); + assert_eq!( + stdout, + "sample.rs:1:12: removable line comment: // TODO remove\n" + ); +} + +#[test] +fn no_preview_restores_the_bare_reported_line() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // TODO remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["check", "--no-preview", "sample.rs"]); + assert_eq!(output.status.code(), Some(1)); + assert_eq!( + String::from_utf8(output.stdout).unwrap(), + "sample.rs:1:12: removable line comment\n" + ); +} + +/// `scan` previews too, and a multi-line comment stays on one line. +#[test] +fn scan_previews_the_comment_text_folded_onto_one_line() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("block.c"), + b"int x; /* first\n second */\n", + ) + .unwrap(); + let output = run(directory.path(), &["scan", "block.c"]); + assert_eq!(output.status.code(), Some(0)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.starts_with("block.c:1:8: block "), + "scan output is:\n{stdout}" + ); + assert!( + stdout.contains("7..28: /* first second */\n"), + "scan output is:\n{stdout}" + ); + assert_eq!(stdout.lines().count(), 1, "scan output is:\n{stdout}"); +} + +/// A comment carrying terminal escapes must not be able to drive the terminal. +#[test] +fn a_previewed_comment_cannot_inject_escape_sequences() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("evil.rs"), + b"let x = 1; // \x1b[31mred\x1b[0m boom\n", + ) + .unwrap(); + let output = run(directory.path(), &["check", "evil.rs"]); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + !stdout.contains('\x1b'), + "an escape byte reached the terminal:\n{stdout:?}" + ); + assert!( + stdout.contains("// \u{fffd}[31mred\u{fffd}[0m boom"), + "check output is:\n{stdout:?}" + ); +} + +/// A long comment is cut to a readable width rather than flooding the report. +#[test] +fn a_long_comment_preview_is_truncated_with_an_ellipsis() { + let directory = tempfile::tempdir().unwrap(); + let comment = "x".repeat(200); + fs::write( + directory.path().join("long.rs"), + format!("let x = 1; // {comment}\n"), + ) + .unwrap(); + let output = run(directory.path(), &["check", "long.rs"]); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + let (_, preview) = stdout.trim_end().rsplit_once(": ").unwrap(); + assert!(preview.ends_with('…'), "preview is:\n{preview}"); + assert_eq!(preview.chars().count(), 72, "preview is:\n{preview}"); +} + +#[test] +fn help_documents_the_preview_switch() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["check", "--help"]); + assert_eq!(output.status.code(), Some(0)); + let help = String::from_utf8(output.stdout).unwrap(); + assert!( + help.contains("--no-preview"), + "`check --help` lacks --no-preview:\n{help}" + ); +} diff --git a/rust/ocomment/tests/lsp.rs b/rust/ocomment/tests/lsp.rs index 2bc5456..e724d96 100644 --- a/rust/ocomment/tests/lsp.rs +++ b/rust/ocomment/tests/lsp.rs @@ -449,3 +449,64 @@ fn on_save_is_opt_in_and_returns_annotated_safe_edits() { assert_eq!(response["result"][0]["range"]["start"]["character"], 11); client.stop(); } + +#[test] +fn diagnostics_and_hover_name_comment_kinds_in_canonical_spelling() { + let workspace = tempfile::tempdir().unwrap(); + let uri = Url::from_file_path(workspace.path().join("doc.rs")).unwrap(); + let mut client = LspClient::start(workspace.path()); + let _ = client.initialize(workspace.path(), &["utf-8"]); + client.send(json!({ + "jsonrpc": "2.0", "method": "textDocument/didOpen", + "params": { "textDocument": { + "uri": uri, "languageId": "rust", "version": 1, + "text": "/** doc */\n// SPDX-License-Identifier: MIT\n" + }} + })); + let pushed = client.notification("textDocument/publishDiagnostics"); + let diagnostics = pushed["params"]["diagnostics"].as_array().unwrap(); + assert_eq!(diagnostics[0]["message"], "removable doc-block comment"); + assert_eq!(diagnostics[0]["code"], "removable-comment"); + + client.send(json!({ + "jsonrpc": "2.0", "id": 60, "method": "textDocument/hover", + "params": { + "textDocument": { "uri": uri }, + "position": { "line": 0, "character": 3 } + } + })); + assert_eq!( + client.response(60)["result"]["contents"], + "OComment: removable doc-block comment" + ); + + client.stop(); +} + +#[test] +fn hover_over_a_protected_comment_names_the_kind_and_reason() { + let workspace = tempfile::tempdir().unwrap(); + let uri = Url::from_file_path(workspace.path().join("preamble.py")).unwrap(); + let mut client = LspClient::start(workspace.path()); + let _ = client.initialize(workspace.path(), &["utf-8"]); + client.send(json!({ + "jsonrpc": "2.0", "method": "textDocument/didOpen", + "params": { "textDocument": { + "uri": uri, "languageId": "python", "version": 1, + "text": "#!/usr/bin/env python3\nx = 1\n" + }} + })); + let _ = client.notification("textDocument/publishDiagnostics"); + client.send(json!({ + "jsonrpc": "2.0", "id": 61, "method": "textDocument/hover", + "params": { + "textDocument": { "uri": uri }, + "position": { "line": 0, "character": 3 } + } + })); + assert_eq!( + client.response(61)["result"]["contents"], + "OComment: kept shebang comment: required source preamble" + ); + client.stop(); +} diff --git a/tools/release_extras.py b/tools/release_extras.py index 5b2ae6f..bc1784b 100755 --- a/tools/release_extras.py +++ b/tools/release_extras.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 -"""Generate shell completions and copy stable documentation for release archives.""" +"""Generate the shell completions and the manual page for release archives.""" from __future__ import annotations import argparse import pathlib -import shutil import subprocess @@ -34,7 +33,12 @@ def main() -> int: capture_output=True, ) (args.output / filename).write_bytes(completed.stdout) - shutil.copyfile(ROOT / "docs/ocomment.1", args.output / "ocomment.1") + page = subprocess.run( + [str(args.binary), "man"], + check=True, + capture_output=True, + ) + (args.output / "ocomment.1").write_bytes(page.stdout) return 0 From 4374d0f1177f08435da72626fa0fc604fc6bd001 Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:56:57 +0900 Subject: [PATCH 02/21] feat(cli): stdin `-` target, fix --dry-run, live --progress, quiet broken pipes, summary wording, man roff sections - `-` reads standard input for check/diff/scan (reported as ); fix refuses it and points at `ocomment strip` - fix --dry-run prints the patch and "Would remove N comments" without writing - --progress draws a live "scanning n/total files" counter on stderr (auto = TTY), cleared before output - closed stdout/stderr pipes end the run quietly with exit 0; the detection is scoped to the program's own output via an OutputPipeClosed marker so a dying `git hash-object` child during fix --staged still fails with exit 2 - no println!/eprintln! left on output paths; stdout is buffered and every write error is reported - summary: proper pluralization, explicit skips not double counted, "Nothing to check/fix/diff/scan", I/O error count, call to action keyed on count; -v only for human format; diff -q / scan -q keep their stdout product - preview also neutralizes bidi/format controls (U+200E/F, U+202A-E, U+2066-9, U+2028/9, U+061C, U+FEFF) and caps characters - man page renders real EXIT STATUS/FILES/EXAMPLES/SEE ALSO sections Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RLSWPMQ9PvtGi3jyBF7bJx --- docs/ocomment.1 | 62 ++- release-extras/_ocomment | 51 ++- release-extras/_ocomment.ps1 | 41 +- release-extras/ocomment.1 | 62 ++- release-extras/ocomment.bash | 2 +- release-extras/ocomment.elv | 41 +- release-extras/ocomment.fish | 41 +- rust/ocomment/src/cli.rs | 486 ++++++++++++++++---- rust/ocomment/src/files.rs | 68 +++ rust/ocomment/src/git.rs | 13 +- rust/ocomment/src/main.rs | 74 ++- rust/ocomment/src/output.rs | 416 +++++++++++++---- rust/ocomment/src/plugin.rs | 52 ++- rust/ocomment/tests/cli.rs | 866 +++++++++++++++++++++++++++++++++-- 14 files changed, 1908 insertions(+), 367 deletions(-) diff --git a/docs/ocomment.1 b/docs/ocomment.1 index 9ec68fd..5dba5e4 100644 --- a/docs/ocomment.1 +++ b/docs/ocomment.1 @@ -19,7 +19,7 @@ Print help (see a summary with \*(Aq\-h\*(Aq) Print version .TP [\fIPATH\fR] -Files or directories to check (default: current directory) +Files or directories to check; `\-` reads standard input (default: current directory) .SH POLICY .TP \fB\-\-policy\fR \fI\fR @@ -254,7 +254,7 @@ never Omit the one\-line comment text from human `check` and `scan` lines .TP \fB\-\-progress\fR \fI\fR [default: auto] -Accepted for compatibility; the end\-of\-run summary replaced this line +When to draw the live scanning counter on standard error .br .br @@ -316,28 +316,40 @@ Render the roff manual page to stdout .TP ocomment\-help(1) Print this message or the help of the given subcommand(s) -.SH EXTRA -EXIT STATUS - 0 Nothing removable was found and every requested change was applied. - 1 Removable comments were reported, or a diff was printed. - 2 Invalid source, configuration, plugin, or I/O failure. - -FILES - .ocomment.toml Project configuration, merged over the user file. - .ocommentignore Extra ignore patterns honoured by repository walks. - .ocomment.lock Pinned digests of the installed WASM scanner plugins. - $XDG_CONFIG_HOME/ocomment/config.toml - User configuration, merged over the built\-in defaults. - -EXAMPLES - ocomment - Check the current repository and report removable comments. - ocomment fix \-\-policy all \-\-layout compact src - Remove every comment under src and close the gaps it leaves. - ocomment strip \-\-language rust < before.rs > after.rs - Strip one file from standard input to standard output. - -SEE ALSO - The complete schemas and guides are available in the OComment repository. .SH VERSION v0.1.0 +.SH EXIT STATUS +.TP +.B 0 +Nothing removable was found and every requested change was applied. +.TP +.B 1 +Removable comments were reported, or a diff was printed. +.TP +.B 2 +Invalid source, configuration, plugin, or I/O failure. +.SH FILES +.TP +.B \&.ocomment.toml +Project configuration, merged over the user file. +.TP +.B \&.ocommentignore +Extra ignore patterns honoured by repository walks. +.TP +.B \&.ocomment.lock +Pinned digests of the installed WASM scanner plugins. +.TP +.B $XDG_CONFIG_HOME/ocomment/config.toml +User configuration, merged over the built\-in defaults. +.SH EXAMPLES +.TP +.B ocomment +Check the current repository and report removable comments. +.TP +.B ocomment fix \-\-policy all \-\-layout compact src +Remove every comment under src and close the gaps it leaves. +.TP +.B ocomment strip \-\-language rust < before.rs > after.rs +Strip one file from standard input to standard output. +.SH SEE ALSO +The complete schemas and guides are available in the OComment repository. diff --git a/release-extras/_ocomment b/release-extras/_ocomment index efcdcf6..b878433 100644 --- a/release-extras/_ocomment +++ b/release-extras/_ocomment @@ -78,7 +78,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -90,7 +90,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--help[Print help (see more with '\''--help'\'')]' \ '-V[Print version]' \ '--version[Print version]' \ -'::paths -- Files or directories to check (default\: current directory):_files' \ +'::paths -- Files or directories to check; `-` reads standard input (default\: current directory):_files' \ ":: :_ocomment_commands" \ "*::: :->ocomment" \ && ret=0 @@ -165,7 +165,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--staged[Read and update Git index blobs rather than treating the working tree as the source]' \ '--index-only[With \`--staged\`, do not attempt a uniquely mappable working-tree update]' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ @@ -177,7 +177,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ '--help[Print help (see more with '\''--help'\'')]' \ -'*::paths -- Files or directories to process (default\: current directory):_files' \ +'*::paths -- Files or directories to process; `-` reads standard input (default\: current directory):_files' \ && ret=0 ;; (fix) @@ -245,9 +245,10 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--staged[Read and update Git index blobs rather than treating the working tree as the source]' \ '--index-only[With \`--staged\`, do not attempt a uniquely mappable working-tree update]' \ +'--dry-run[Print the patch \`fix\` would apply and write nothing]' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -257,7 +258,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ '--help[Print help (see more with '\''--help'\'')]' \ -'*::paths -- Files or directories to process (default\: current directory):_files' \ +'*::paths -- Files or directories to rewrite (default\: current directory):_files' \ && ret=0 ;; (diff) @@ -325,7 +326,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--staged[Read and update Git index blobs rather than treating the working tree as the source]' \ '--index-only[With \`--staged\`, do not attempt a uniquely mappable working-tree update]' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ @@ -337,7 +338,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ '--help[Print help (see more with '\''--help'\'')]' \ -'*::paths -- Files or directories to process (default\: current directory):_files' \ +'*::paths -- Files or directories to process; `-` reads standard input (default\: current directory):_files' \ && ret=0 ;; (scan) @@ -405,7 +406,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--staged[Read and update Git index blobs rather than treating the working tree as the source]' \ '--index-only[With \`--staged\`, do not attempt a uniquely mappable working-tree update]' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ @@ -417,7 +418,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ '--help[Print help (see more with '\''--help'\'')]' \ -'*::paths -- Files or directories to process (default\: current directory):_files' \ +'*::paths -- Files or directories to process; `-` reads standard input (default\: current directory):_files' \ && ret=0 ;; (strip) @@ -485,7 +486,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -562,7 +563,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -639,7 +640,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--fix[For the Lefthook hook, run \`fix\` instead of \`check\`]' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ @@ -718,7 +719,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -796,7 +797,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -873,7 +874,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -961,7 +962,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -1039,7 +1040,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -1117,7 +1118,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -1194,7 +1195,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -1272,7 +1273,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -1350,7 +1351,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -1476,7 +1477,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -1554,7 +1555,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ @@ -1631,7 +1632,7 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--format=[Output encoding]:FORMAT:(human json jsonl sarif github)' \ '--color=[When to colour terminal output]:WHEN:(auto always never)' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ -'--progress=[Accepted for compatibility; the end-of-run summary replaced this line]:WHEN:(auto always never)' \ +'--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ diff --git a/release-extras/_ocomment.ps1 b/release-extras/_ocomment.ps1 index ccd9dcb..9393203 100644 --- a/release-extras/_ocomment.ps1 +++ b/release-extras/_ocomment.ps1 @@ -31,7 +31,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -70,7 +70,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--staged', '--staged', [CompletionResultType]::ParameterName, 'Read and update Git index blobs rather than treating the working tree as the source') [CompletionResult]::new('--index-only', '--index-only', [CompletionResultType]::ParameterName, 'With `--staged`, do not attempt a uniquely mappable working-tree update') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') @@ -95,9 +95,10 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--staged', '--staged', [CompletionResultType]::ParameterName, 'Read and update Git index blobs rather than treating the working tree as the source') [CompletionResult]::new('--index-only', '--index-only', [CompletionResultType]::ParameterName, 'With `--staged`, do not attempt a uniquely mappable working-tree update') + [CompletionResult]::new('--dry-run', '--dry-run', [CompletionResultType]::ParameterName, 'Print the patch `fix` would apply and write nothing') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -120,7 +121,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--staged', '--staged', [CompletionResultType]::ParameterName, 'Read and update Git index blobs rather than treating the working tree as the source') [CompletionResult]::new('--index-only', '--index-only', [CompletionResultType]::ParameterName, 'With `--staged`, do not attempt a uniquely mappable working-tree update') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') @@ -145,7 +146,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--staged', '--staged', [CompletionResultType]::ParameterName, 'Read and update Git index blobs rather than treating the working tree as the source') [CompletionResult]::new('--index-only', '--index-only', [CompletionResultType]::ParameterName, 'With `--staged`, do not attempt a uniquely mappable working-tree update') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') @@ -170,7 +171,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -193,7 +194,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -216,7 +217,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--fix', '--fix', [CompletionResultType]::ParameterName, 'For the Lefthook hook, run `fix` instead of `check`') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') @@ -240,7 +241,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -263,7 +264,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -286,7 +287,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -319,7 +320,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -342,7 +343,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -365,7 +366,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -388,7 +389,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -411,7 +412,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -434,7 +435,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -488,7 +489,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -511,7 +512,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') @@ -534,7 +535,7 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--format', '--format', [CompletionResultType]::ParameterName, 'Output encoding') [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to colour terminal output') [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') - [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'Accepted for compatibility; the end-of-run summary replaced this line') + [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') diff --git a/release-extras/ocomment.1 b/release-extras/ocomment.1 index 9ec68fd..5dba5e4 100644 --- a/release-extras/ocomment.1 +++ b/release-extras/ocomment.1 @@ -19,7 +19,7 @@ Print help (see a summary with \*(Aq\-h\*(Aq) Print version .TP [\fIPATH\fR] -Files or directories to check (default: current directory) +Files or directories to check; `\-` reads standard input (default: current directory) .SH POLICY .TP \fB\-\-policy\fR \fI\fR @@ -254,7 +254,7 @@ never Omit the one\-line comment text from human `check` and `scan` lines .TP \fB\-\-progress\fR \fI\fR [default: auto] -Accepted for compatibility; the end\-of\-run summary replaced this line +When to draw the live scanning counter on standard error .br .br @@ -316,28 +316,40 @@ Render the roff manual page to stdout .TP ocomment\-help(1) Print this message or the help of the given subcommand(s) -.SH EXTRA -EXIT STATUS - 0 Nothing removable was found and every requested change was applied. - 1 Removable comments were reported, or a diff was printed. - 2 Invalid source, configuration, plugin, or I/O failure. - -FILES - .ocomment.toml Project configuration, merged over the user file. - .ocommentignore Extra ignore patterns honoured by repository walks. - .ocomment.lock Pinned digests of the installed WASM scanner plugins. - $XDG_CONFIG_HOME/ocomment/config.toml - User configuration, merged over the built\-in defaults. - -EXAMPLES - ocomment - Check the current repository and report removable comments. - ocomment fix \-\-policy all \-\-layout compact src - Remove every comment under src and close the gaps it leaves. - ocomment strip \-\-language rust < before.rs > after.rs - Strip one file from standard input to standard output. - -SEE ALSO - The complete schemas and guides are available in the OComment repository. .SH VERSION v0.1.0 +.SH EXIT STATUS +.TP +.B 0 +Nothing removable was found and every requested change was applied. +.TP +.B 1 +Removable comments were reported, or a diff was printed. +.TP +.B 2 +Invalid source, configuration, plugin, or I/O failure. +.SH FILES +.TP +.B \&.ocomment.toml +Project configuration, merged over the user file. +.TP +.B \&.ocommentignore +Extra ignore patterns honoured by repository walks. +.TP +.B \&.ocomment.lock +Pinned digests of the installed WASM scanner plugins. +.TP +.B $XDG_CONFIG_HOME/ocomment/config.toml +User configuration, merged over the built\-in defaults. +.SH EXAMPLES +.TP +.B ocomment +Check the current repository and report removable comments. +.TP +.B ocomment fix \-\-policy all \-\-layout compact src +Remove every comment under src and close the gaps it leaves. +.TP +.B ocomment strip \-\-language rust < before.rs > after.rs +Strip one file from standard input to standard output. +.SH SEE ALSO +The complete schemas and guides are available in the OComment repository. diff --git a/release-extras/ocomment.bash b/release-extras/ocomment.bash index 3252c8e..81a8f2e 100644 --- a/release-extras/ocomment.bash +++ b/release-extras/ocomment.bash @@ -515,7 +515,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__fix) - opts="-q -v -h --staged --index-only --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" + opts="-q -v -h --staged --index-only --dry-run --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/release-extras/ocomment.elv b/release-extras/ocomment.elv index 5c6a641..715e11b 100644 --- a/release-extras/ocomment.elv +++ b/release-extras/ocomment.elv @@ -28,7 +28,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -66,7 +66,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --staged 'Read and update Git index blobs rather than treating the working tree as the source' cand --index-only 'With `--staged`, do not attempt a uniquely mappable working-tree update' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' @@ -90,9 +90,10 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --staged 'Read and update Git index blobs rather than treating the working tree as the source' cand --index-only 'With `--staged`, do not attempt a uniquely mappable working-tree update' + cand --dry-run 'Print the patch `fix` would apply and write nothing' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -114,7 +115,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --staged 'Read and update Git index blobs rather than treating the working tree as the source' cand --index-only 'With `--staged`, do not attempt a uniquely mappable working-tree update' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' @@ -138,7 +139,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --staged 'Read and update Git index blobs rather than treating the working tree as the source' cand --index-only 'With `--staged`, do not attempt a uniquely mappable working-tree update' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' @@ -162,7 +163,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -184,7 +185,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -206,7 +207,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --fix 'For the Lefthook hook, run `fix` instead of `check`' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' @@ -229,7 +230,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -251,7 +252,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -273,7 +274,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -305,7 +306,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -327,7 +328,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -349,7 +350,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -371,7 +372,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -393,7 +394,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -415,7 +416,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -460,7 +461,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -482,7 +483,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' @@ -504,7 +505,7 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --format 'Output encoding' cand --color 'When to colour terminal output' cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' - cand --progress 'Accepted for compatibility; the end-of-run summary replaced this line' + cand --progress 'When to draw the live scanning counter on standard error' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' diff --git a/release-extras/ocomment.fish b/release-extras/ocomment.fish index 01c2091..5b99baa 100644 --- a/release-extras/ocomment.fish +++ b/release-extras/ocomment.fish @@ -95,7 +95,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_needs_command" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_needs_command" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_needs_command" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_needs_command" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' @@ -190,7 +190,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l staged -d 'Read and update Git index blobs rather than treating the working tree as the source' @@ -272,11 +272,12 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l staged -d 'Read and update Git index blobs rather than treating the working tree as the source' complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l index-only -d 'With `--staged`, do not attempt a uniquely mappable working-tree update' +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l dry-run -d 'Print the patch `fix` would apply and write nothing' complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' @@ -354,7 +355,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l staged -d 'Read and update Git index blobs rather than treating the working tree as the source' @@ -436,7 +437,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l staged -d 'Read and update Git index blobs rather than treating the working tree as the source' @@ -518,7 +519,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' @@ -598,7 +599,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' @@ -678,7 +679,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l fix -d 'For the Lefthook hook, run `fix` instead of `check`' @@ -759,7 +760,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' @@ -839,7 +840,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' @@ -919,7 +920,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' @@ -1009,7 +1010,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' @@ -1089,7 +1090,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' @@ -1169,7 +1170,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' @@ -1249,7 +1250,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' @@ -1329,7 +1330,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' @@ -1409,7 +1410,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' @@ -1496,7 +1497,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' @@ -1576,7 +1577,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' @@ -1656,7 +1657,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l hyperlinks -d 'When to emit terminal hyperlinks for reported paths' -r -f -a "auto\t'' always\t'' never\t''" -complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l progress -d 'Accepted for compatibility; the end-of-run summary replaced this line' -r -f -a "auto\t'' +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l progress -d 'When to draw the live scanning counter on standard error' -r -f -a "auto\t'' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' diff --git a/rust/ocomment/src/cli.rs b/rust/ocomment/src/cli.rs index 5c8cf76..7e0fe14 100644 --- a/rust/ocomment/src/cli.rs +++ b/rust/ocomment/src/cli.rs @@ -7,7 +7,7 @@ use crate::{ plugin, values::{CommentKindArg, DialectArg, LanguageArg, LayoutArg, PolicyArg}, }; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum}; use clap_complete::{Shell, generate}; use ocomment_core::{CommentKind, Dialect, Language, transform}; @@ -16,6 +16,7 @@ use std::{ fs, io::{self, IsTerminal, Read, Write}, path::PathBuf, + sync::atomic::{AtomicBool, AtomicUsize, Ordering}, }; const LONG_ABOUT: &str = "\ @@ -48,6 +49,46 @@ EXAMPLES SEE ALSO The complete schemas and guides are available in the OComment repository."; +/// The roff sections `clap_mangen` cannot derive, carrying the same content as +/// the `--help` epilogue above. A line that would start with `.` is escaped +/// with `\&` so roff reads a file name as text rather than as a macro. +const MAN_SECTIONS: &str = r#".SH EXIT STATUS +.TP +.B 0 +Nothing removable was found and every requested change was applied. +.TP +.B 1 +Removable comments were reported, or a diff was printed. +.TP +.B 2 +Invalid source, configuration, plugin, or I/O failure. +.SH FILES +.TP +.B \&.ocomment.toml +Project configuration, merged over the user file. +.TP +.B \&.ocommentignore +Extra ignore patterns honoured by repository walks. +.TP +.B \&.ocomment.lock +Pinned digests of the installed WASM scanner plugins. +.TP +.B $XDG_CONFIG_HOME/ocomment/config.toml +User configuration, merged over the built\-in defaults. +.SH EXAMPLES +.TP +.B ocomment +Check the current repository and report removable comments. +.TP +.B ocomment fix \-\-policy all \-\-layout compact src +Remove every comment under src and close the gaps it leaves. +.TP +.B ocomment strip \-\-language rust < before.rs > after.rs +Strip one file from standard input to standard output. +.SH SEE ALSO +The complete schemas and guides are available in the OComment repository. +"#; + #[derive(Parser)] #[command( name = "ocomment", @@ -58,7 +99,7 @@ SEE ALSO #[command(args_conflicts_with_subcommands = true)] #[command(after_long_help = AFTER_LONG_HELP)] struct Cli { - /// Files or directories to check (default: current directory). + /// Files or directories to check; `-` reads standard input (default: current directory). #[arg(value_name = "PATH")] paths: Vec, /// The command to run; `check` runs when none is given. @@ -170,10 +211,7 @@ struct OutputArgs { /// Omit the one-line comment text from human `check` and `scan` lines. #[arg(long, global = true)] no_preview: bool, - /// Accepted for compatibility; the end-of-run summary replaced this line. - // Nothing reads the value: the summary is written whether or not standard - // error is a terminal. - #[allow(dead_code)] + /// When to draw the live scanning counter on standard error. #[arg(long, global = true, value_enum, default_value_t, value_name = "WHEN")] progress: AutoChoice, /// Print nothing but errors and diagnostics. @@ -226,7 +264,7 @@ enum Command { /// Report removable comments (default command) Check(TargetArgs), /// Remove comments in place through an atomic, rollback-backed transaction - Fix(TargetArgs), + Fix(FixArgs), /// Print a unified diff of the changes fix would make Diff(TargetArgs), /// List every comment with its kind, disposition and byte span @@ -256,9 +294,17 @@ enum Command { #[derive(Clone, Debug, Default, Args)] struct TargetArgs { - /// Files or directories to process (default: current directory). + /// Files or directories to process; `-` reads standard input (default: current directory). #[arg(value_name = "PATH")] paths: Vec, + /// Whether the Git index, rather than the working tree, is the source. + #[command(flatten)] + git: GitArgs, +} + +/// The `--staged` pair, shared by every command that can read the Git index. +#[derive(Clone, Debug, Default, Args)] +struct GitArgs { /// Read and update Git index blobs rather than treating the working tree as the source. #[arg(long)] staged: bool, @@ -267,6 +313,32 @@ struct TargetArgs { index_only: bool, } +#[derive(Args)] +struct FixArgs { + // `fix` rewrites files in place and refuses the `-` that stands for + // standard input, so its PATH list is not the one every other command + // takes and does not borrow that command's help line. + /// Files or directories to rewrite (default: current directory). + #[arg(value_name = "PATH")] + paths: Vec, + /// Whether the Git index, rather than the working tree, is rewritten. + #[command(flatten)] + git: GitArgs, + /// Print the patch `fix` would apply and write nothing. + #[arg(long)] + dry_run: bool, +} + +impl FixArgs { + /// The same targets in the shape every other command hands to the run. + fn target(self) -> TargetArgs { + TargetArgs { + paths: self.paths, + git: self.git, + } + } +} + #[derive(Args)] struct InitArgs { /// Which starter file to write. @@ -358,56 +430,71 @@ pub fn run() -> Result { ..Default::default() }, &common, + false, ), - Some(Command::Check(args)) => run_target(Operation::Check, args, &common), - Some(Command::Fix(args)) => run_target(Operation::Fix, args, &common), - Some(Command::Diff(args)) => run_target(Operation::Diff, args, &common), - Some(Command::Scan(args)) => run_target(Operation::Scan, args, &common), + Some(Command::Check(args)) => run_target(Operation::Check, args, &common, false), + // `--dry-run` runs the diff and reports it in fix vocabulary: the two + // commands must agree on the patch, so only the wording differs. + Some(Command::Fix(args)) if args.dry_run => { + run_target(Operation::Diff, args.target(), &common, true) + } + Some(Command::Fix(args)) => run_target(Operation::Fix, args.target(), &common, false), + Some(Command::Diff(args)) => run_target(Operation::Diff, args, &common, false), + Some(Command::Scan(args)) => run_target(Operation::Scan, args, &common, false), Some(Command::Strip) => run_strip(&common), Some(Command::Lsp) => lsp::run(common.config.as_deref()), Some(Command::Init(args)) => run_init(args), Some(Command::Config(args)) => run_config(args, &common), - Some(Command::Languages) => { - print_languages(); - Ok(0) - } + Some(Command::Languages) => print_languages(), Some(Command::Plugin(args)) => run_plugin(args, &common), - Some(Command::Completions { shell }) => { - generate(shell, &mut Cli::command(), "ocomment", &mut io::stdout()); - Ok(0) - } + Some(Command::Completions { shell }) => run_completions(shell), Some(Command::Doctor) => run_doctor(&common), Some(Command::Man) => run_man(), } } -fn run_target(operation: Operation, args: TargetArgs, common: &CommonArgs) -> Result { +fn run_target( + operation: Operation, + args: TargetArgs, + common: &CommonArgs, + dry_run: bool, +) -> Result { let mut resolved = config::load(common.config.as_deref())?; apply_cli_overrides(&mut resolved.config, common); let plugin_host = plugin::PluginHost::load(&resolved.root, &resolved.config.plugins)?; let presentation = presentation(common); let verbosity = common.verbosity(); - if verbosity == Verbosity::Verbose { - trace_run(&resolved, &args.paths); + // The trace is part of the human report; a machine format keeps standard + // error empty however loud the run was asked to be. + if verbosity == Verbosity::Verbose && common.output.format == OutputFormat::Human { + trace_run(&resolved, &args.paths)?; } - let staged = args.staged || resolved.config.git.staged; + let progress = progress_enabled(common); + let staged = args.git.staged || resolved.config.git.staged; + // `fix --dry-run` writes nothing, but it is still the command whose job is + // to rewrite files in place, and standard input cannot be rewritten. + let rewrites = operation == Operation::Fix || dry_run; + let (paths, stdin) = target_paths(&args.paths, rewrites, staged)?; if staged { return git::run_staged(git::StagedRequest { operation, - paths: &args.paths, + paths: &paths, resolved: &resolved, format: common.output.format, - index_only: args.index_only || resolved.config.git.index_only, + index_only: args.git.index_only || resolved.config.git.index_only, plugin_host: &plugin_host, forced_language: common.language(), forced_dialect: common.dialect(), presentation, verbosity, preview: !common.output.no_preview, + dry_run, }); } - let discovery = files::discover(&args.paths, &resolved, common.language(), common.dialect())?; - let files: Vec<_> = discovery + let discovery = read_targets(&paths, stdin, &resolved, common)?; + let total = discovery.files.len(); + let counter = Progress::default(); + let processed = discovery .files .par_iter() .map(|file| { @@ -434,6 +521,9 @@ fn run_target(operation: Operation, args: TargetArgs, common: &CommonArgs) -> Re } else { transform(&file.source, language, options) }; + if progress { + counter.report(total); + } Ok::<_, anyhow::Error>(ProcessedFile { path: file.path.clone(), source: file.source.clone(), @@ -441,7 +531,11 @@ fn run_target(operation: Operation, args: TargetArgs, common: &CommonArgs) -> Re result, }) }) - .collect::>>()?; + .collect::>>(); + if progress { + counter.clear(); + } + let files = processed?; let report_invalid = output::invalid(&files); let io_invalid = discovery.skipped.iter().any(|item| item.error); @@ -470,7 +564,7 @@ fn run_target(operation: Operation, args: TargetArgs, common: &CommonArgs) -> Re verbosity, preview: !common.output.no_preview, explain: false, - dry_run: false, + dry_run, force_invalid: resolved.config.policy.force_invalid, applied, }, @@ -484,6 +578,79 @@ fn run_target(operation: Operation, args: TargetArgs, common: &CommonArgs) -> Re } } +/// How the PATH list names standard input. +const STDIN_ARGUMENT: &str = "-"; + +/// Split the requested targets into ordinary paths and the `-` that stands for +/// standard input, refusing the combinations that cannot be honoured. +fn target_paths(paths: &[PathBuf], rewrites: bool, staged: bool) -> Result<(Vec, bool)> { + let is_stdin = |path: &PathBuf| path.as_os_str() == STDIN_ARGUMENT; + match paths.iter().filter(|path| is_stdin(path)).count() { + 0 => return Ok((paths.to_vec(), false)), + 1 => {} + // A pipe is consumed once; a second `-` would silently report the same + // bytes twice or nothing at all. + _ => bail!("cannot read standard input twice; `-` may appear only once"), + } + if rewrites { + bail!("cannot rewrite standard input in place; use `ocomment strip`"); + } + if staged { + bail!("cannot read standard input with --staged; the index is the source"); + } + Ok(( + paths + .iter() + .filter(|path| !is_stdin(path)) + .cloned() + .collect(), + true, + )) +} + +/// Discover the named paths and, when `-` was among them, fold the bytes read +/// from standard input in as one more file so a piped run takes exactly the +/// same reporting path as a walked one. +fn read_targets( + paths: &[PathBuf], + stdin: bool, + resolved: &config::ResolvedConfig, + common: &CommonArgs, +) -> Result { + if !stdin { + return files::discover(paths, resolved, common.language(), common.dialect()); + } + // An empty list means "the whole repository" only when no target was named + // at all; `-` on its own is a target, and walking would ignore it. + let mut discovery = if paths.is_empty() { + files::Discovery::default() + } else { + files::discover(paths, resolved, common.language(), common.dialect())? + }; + let mut bytes = Vec::new(); + io::stdin() + .lock() + .read_to_end(&mut bytes) + .context("cannot read standard input")?; + match files::stdin_source(bytes, resolved, common.language(), common.dialect()) { + Ok(file) => discovery.files.push(file), + // A skip that cannot be reported per file — nothing was named to skip + // — is a usage error the run must not swallow. + Err(skipped) if skipped.error => { + let reason = skipped.reason; + bail!("{reason}") + } + Err(skipped) => discovery.skipped.push(skipped), + } + discovery + .files + .sort_by(|left, right| left.path.cmp(&right.path)); + discovery + .skipped + .sort_by(|left, right| left.path.cmp(&right.path)); + Ok(discovery) +} + fn run_strip(common: &CommonArgs) -> Result { let mut source = Vec::new(); io::stdin() @@ -499,27 +666,34 @@ fn run_strip(common: &CommonArgs) -> Result { ocomment_core::detect_language(None, &source) .map(|value| (value.language, value.dialect)) }) - .context("cannot detect stdin language; pass --language")?; - let (language, mut options) = - resolved.for_path(std::path::Path::new(""), detection.0, detection.1); + .context(files::STDIN_LANGUAGE_HELP)?; + let (language, mut options) = resolved.for_path( + std::path::Path::new(files::STDIN_PATH), + detection.0, + detection.1, + ); if let Some(value) = common.dialect() { config::validate_dialect(language, value)?; options.scan.dialect = value; } let result = transform(&source, language, options); + let stderr = io::stderr(); + let mut report = stderr.lock(); for diagnostic in &result.report.diagnostics { - eprintln!( - "stdin:{}..{}: {}: {}", - diagnostic.span.start, diagnostic.span.end, diagnostic.code, diagnostic.message - ); + output::note( + &mut report, + &format!( + "stdin:{}..{}: {}: {}", + diagnostic.span.start, diagnostic.span.end, diagnostic.code, diagnostic.message + ), + )?; } if !result.report.valid && !resolved.config.policy.force_invalid { return Ok(2); } - io::stdout() - .lock() - .write_all(&result.output) - .context("cannot write standard output")?; + let mut stdout = output::stdout(); + output::wrote(stdout.write_all(&result.output))?; + output::finish(&mut stdout)?; Ok(if result.report.valid { 0 } else { 2 }) } @@ -554,21 +728,46 @@ fn apply_cli_overrides(config: &mut config::Config, common: &CommonArgs) { /// Render the roff manual page from the parser definition itself. fn run_man() -> Result { let mut page = Vec::new(); - clap_mangen::Man::new(Cli::command()) + // `clap_mangen` renders `after_long_help` as one opaque `.SH EXTRA` body, + // so the page is built without it and the same content is appended below + // as real roff sections. + // + // The `.TH` date is left blank on purpose: stamping the build date would + // make two reproducible builds of the same source disagree. + clap_mangen::Man::new(Cli::command().after_long_help(None)) .title("OCOMMENT") .manual("User Commands") .render(&mut page) .context("cannot render the manual page")?; - io::stdout() - .lock() - .write_all(&page) - .context("cannot write standard output")?; + if !page.ends_with(b"\n") { + page.push(b'\n'); + } + page.extend_from_slice(MAN_SECTIONS.as_bytes()); + let mut stdout = output::stdout(); + output::wrote(stdout.write_all(&page))?; + output::finish(&mut stdout)?; + Ok(0) +} + +/// Write the shell completion script. +/// +/// `clap_complete` writes straight into the handle it is given and panics if +/// that write fails, so it is given a buffer in memory and the one write that +/// can fail is made here. +fn run_completions(shell: Shell) -> Result { + let mut script = Vec::new(); + generate(shell, &mut Cli::command(), "ocomment", &mut script); + let mut stdout = output::stdout(); + output::wrote(stdout.write_all(&script))?; + output::finish(&mut stdout)?; Ok(0) } fn run_init(args: InitArgs) -> Result { + let mut stdout = output::stdout(); match args.kind { InitKind::Config => create_new( + &mut stdout, config::CONFIG_FILE, include_str!("../assets/default-config.toml"), )?, @@ -579,89 +778,115 @@ fn run_init(args: InitArgs) -> Result { "ocomment check --staged" }; create_new( + &mut stdout, "lefthook.yml", &format!("pre-commit:\n commands:\n ocomment:\n run: {command}\n"), )?; } } + output::finish(&mut stdout)?; Ok(0) } -fn create_new(path: &str, contents: &str) -> Result<()> { +fn create_new(output: &mut impl Write, path: &str, contents: &str) -> Result<()> { let mut file = fs::OpenOptions::new() .write(true) .create_new(true) .open(path) .with_context(|| format!("refusing to overwrite {path}"))?; file.write_all(contents.as_bytes())?; - println!("created {path}"); + output::wrote(writeln!(output, "created {path}"))?; Ok(()) } fn run_config(args: ConfigArgs, common: &CommonArgs) -> Result { + let mut stdout = output::stdout(); match args.action { - ConfigAction::Schema => print!("{}", include_str!("../assets/config.schema.json")), + ConfigAction::Schema => { + output::wrote(write!( + stdout, + "{}", + include_str!("../assets/config.schema.json") + ))?; + } action => { let mut resolved = config::load(common.config.as_deref())?; apply_cli_overrides(&mut resolved.config, common); match action { ConfigAction::Show => { resolved.config.version = Some(1); - print!("{}", toml::to_string_pretty(&resolved.config)?); + output::wrote(write!( + stdout, + "{}", + toml::to_string_pretty(&resolved.config)? + ))?; } ConfigAction::Locate => { if let Some(path) = &resolved.trace.user { - println!("user\t{}", path.display()); + output::wrote(writeln!(stdout, "user\t{}", path.display()))?; } if let Some(path) = &resolved.trace.project { - println!("project\t{}", path.display()); + output::wrote(writeln!(stdout, "project\t{}", path.display()))?; } if let Some(path) = &resolved.trace.explicit { - println!("explicit\t{}", path.display()); + output::wrote(writeln!(stdout, "explicit\t{}", path.display()))?; } if resolved.trace.user.is_none() && resolved.trace.project.is_none() && resolved.trace.explicit.is_none() { - println!("built-in defaults"); + output::wrote(writeln!(stdout, "built-in defaults"))?; } } ConfigAction::Explain => { - println!("precedence: built-in < XDG user < project < path override < CLI"); - println!("root: {}", resolved.root.display()); - println!( + output::wrote(writeln!( + stdout, + "precedence: built-in < XDG user < project < path override < CLI" + ))?; + output::wrote(writeln!(stdout, "root: {}", resolved.root.display()))?; + output::wrote(writeln!( + stdout, "policy: {}; layout: {}", resolved.config.policy.mode, resolved.config.policy.layout - ); + ))?; } ConfigAction::Schema => unreachable!(), } } } + output::finish(&mut stdout)?; Ok(0) } -fn print_languages() { - println!("language\textensions / guaranteed dialects"); - println!("rust\trs"); - println!("ocaml\tml,mli (OCaml 5.5 lexical forms)"); - println!("c\tc,h / standard, GNU, Objective-C"); - println!("cpp\tcc,cpp,cxx,hpp / standard, GNU, Objective-C++, CUDA"); - println!("go\tgo"); - println!("java\tjava (Unicode escape translation)"); - println!("javascript\tjs,mjs,cjs,jsx / ECMAScript, JSX"); - println!("typescript\tts,mts,cts,tsx / TypeScript, TSX"); - println!("python\tpy,pyw,pyi"); - println!("shell\tsh,bash,zsh / POSIX sh, Bash 5.3, zsh"); - println!("html\thtml,htm / recursive script and style"); - println!("css\tcss"); - println!("jsonc\tjsonc,json5"); - println!("sql\tsql / PostgreSQL, MySQL, SQLite, T-SQL, Oracle"); - println!("kotlin\tkt,kts"); +fn print_languages() -> Result { + let mut stdout = output::stdout(); + for line in [ + "language\textensions / guaranteed dialects", + "rust\trs", + "ocaml\tml,mli (OCaml 5.5 lexical forms)", + "c\tc,h / standard, GNU, Objective-C", + "cpp\tcc,cpp,cxx,hpp / standard, GNU, Objective-C++, CUDA", + "go\tgo", + "java\tjava (Unicode escape translation)", + "javascript\tjs,mjs,cjs,jsx / ECMAScript, JSX", + "typescript\tts,mts,cts,tsx / TypeScript, TSX", + "python\tpy,pyw,pyi", + "shell\tsh,bash,zsh / POSIX sh, Bash 5.3, zsh", + "html\thtml,htm / recursive script and style", + "css\tcss", + "jsonc\tjsonc,json5", + "sql\tsql / PostgreSQL, MySQL, SQLite, T-SQL, Oracle", + "kotlin\tkt,kts", + ] { + output::wrote(writeln!(stdout, "{line}"))?; + } + output::finish(&mut stdout)?; + Ok(0) } fn run_plugin(args: PluginArgs, common: &CommonArgs) -> Result { let resolved = config::load(common.config.as_deref())?; + let mut stdout = output::stdout(); match args.command { PluginCommand::Add { source, @@ -669,43 +894,115 @@ fn run_plugin(args: PluginArgs, common: &CommonArgs) -> Result { sha256, identity, } => plugin::add( + &mut stdout, &resolved.root, &source, name.as_deref(), sha256.as_deref(), identity.as_deref(), )?, - PluginCommand::Remove { name } => plugin::remove(&resolved.root, &name)?, - PluginCommand::List => plugin::list(&resolved.root)?, - PluginCommand::Update { name } => plugin::update(&resolved.root, name.as_deref())?, - PluginCommand::Verify { name } => plugin::verify(&resolved.root, name.as_deref())?, - PluginCommand::New { path } => plugin::new_plugin(&path)?, + PluginCommand::Remove { name } => plugin::remove(&mut stdout, &resolved.root, &name)?, + PluginCommand::List => plugin::list(&mut stdout, &resolved.root)?, + PluginCommand::Update { name } => { + plugin::update(&mut stdout, &resolved.root, name.as_deref())?; + } + PluginCommand::Verify { name } => { + plugin::verify(&mut stdout, &resolved.root, name.as_deref())?; + } + PluginCommand::New { path } => plugin::new_plugin(&mut stdout, &path)?, } + output::finish(&mut stdout)?; Ok(0) } fn run_doctor(common: &CommonArgs) -> Result { - println!("ocomment {}", env!("CARGO_PKG_VERSION")); + let mut stdout = output::stdout(); + output::wrote(writeln!(stdout, "ocomment {}", env!("CARGO_PKG_VERSION")))?; let resolved = config::load(common.config.as_deref())?; - println!("configuration: ok (root {})", resolved.root.display()); - println!("languages: {} built in", Language::ALL.len()); + output::wrote(writeln!( + stdout, + "configuration: ok (root {})", + resolved.root.display() + ))?; + output::wrote(writeln!( + stdout, + "languages: {} built in", + Language::ALL.len() + ))?; if std::process::Command::new("git") .arg("--version") .output() .is_ok() { - println!("git: available"); + output::wrote(writeln!(stdout, "git: available"))?; } else { - println!("git: unavailable (only --staged is affected)"); + output::wrote(writeln!( + stdout, + "git: unavailable (only --staged is affected)" + ))?; } - plugin::verify(&resolved.root, None)?; - println!( + plugin::verify(&mut stdout, &resolved.root, None)?; + output::wrote(writeln!( + stdout, "LSP: stdio server available; on-save is opt-in ({})", resolved.config.lsp.on_save - ); + ))?; + output::finish(&mut stdout)?; Ok(0) } +/// How many files may be processed between two redraws of the counter. +const PROGRESS_STEP: usize = 50; + +/// Whether this run draws the live scanning counter. The counter is terminal +/// decoration: it never belongs in a machine format, and `-q` silences it. +fn progress_enabled(common: &CommonArgs) -> bool { + common.output.format == OutputFormat::Human + && common.verbosity() != Verbosity::Quiet + && match common.output.progress { + AutoChoice::Auto => io::stderr().is_terminal(), + AutoChoice::Always => true, + AutoChoice::Never => false, + } +} + +/// The live scanning counter: how many files it has seen, and whether it ever +/// put a line on the screen. +#[derive(Default)] +struct Progress { + scanned: AtomicUsize, + drawn: AtomicBool, +} + +impl Progress { + /// Advance the live `n/total` counter, rewriting one line on standard + /// error rather than scrolling a line for every file. + fn report(&self, total: usize) { + let seen = self.scanned.fetch_add(1, Ordering::Relaxed) + 1; + if !seen.is_multiple_of(PROGRESS_STEP) && seen != total { + return; + } + let mut stderr = io::stderr().lock(); + let _ = write!(stderr, "\rocomment: scanning {seen}/{total} files"); + let _ = stderr.flush(); + self.drawn.store(true, Ordering::Relaxed); + } + + /// Erase the counter so the report that follows starts on a clean line. + /// + /// A run with nothing to scan draws no counter, and erasing a line it + /// never wrote would put an escape sequence on a standard error whose + /// reader was promised only the summary. + fn clear(&self) { + if !self.drawn.load(Ordering::Relaxed) { + return; + } + let mut stderr = io::stderr().lock(); + let _ = write!(stderr, "\r\x1b[2K"); + let _ = stderr.flush(); + } +} + fn presentation(common: &CommonArgs) -> Presentation { let stdout_tty = io::stdout().is_terminal(); let no_color = std::env::var_os("NO_COLOR").is_some(); @@ -726,8 +1023,10 @@ fn presentation(common: &CommonArgs) -> Presentation { /// The `--verbose` header: where the run is rooted, what it was pointed at, /// and which configuration files it merged. -fn trace_run(resolved: &config::ResolvedConfig, paths: &[PathBuf]) { - eprintln!("root: {}", resolved.root.display()); +fn trace_run(resolved: &config::ResolvedConfig, paths: &[PathBuf]) -> Result<()> { + let stderr = io::stderr(); + let mut report = stderr.lock(); + output::note(&mut report, &format!("root: {}", resolved.root.display()))?; let target = if paths.is_empty() { ".".to_owned() } else { @@ -737,7 +1036,7 @@ fn trace_run(resolved: &config::ResolvedConfig, paths: &[PathBuf]) { .collect::>() .join(" ") }; - eprintln!("target: {target}"); + output::note(&mut report, &format!("target: {target}"))?; let trace = &resolved.trace; let sources = [ ("user", &trace.user), @@ -747,11 +1046,12 @@ fn trace_run(resolved: &config::ResolvedConfig, paths: &[PathBuf]) { let mut traced = false; for (label, path) in sources { if let Some(path) = path { - eprintln!("config: {label} {}", path.display()); + output::note(&mut report, &format!("config: {label} {}", path.display()))?; traced = true; } } if !traced { - eprintln!("config: built-in defaults"); + output::note(&mut report, "config: built-in defaults")?; } + Ok(()) } diff --git a/rust/ocomment/src/files.rs b/rust/ocomment/src/files.rs index 8b38806..8268562 100644 --- a/rust/ocomment/src/files.rs +++ b/rust/ocomment/src/files.rs @@ -35,6 +35,74 @@ pub struct Discovery { pub skipped: Vec, } +/// The path standard input is reported under. It is not a real file name: the +/// renderers print it, and the configuration override matcher sees it, exactly +/// as it reads here. +pub const STDIN_PATH: &str = ""; + +/// What both `strip` and a `-` target say when the bytes carry no signature to +/// detect a language from. Standard input has no name to fall back on, so the +/// only way forward is for the caller to name the language. +pub const STDIN_LANGUAGE_HELP: &str = "cannot detect the language of standard input; \ +pass --language (see `ocomment languages`)"; + +/// Turn the bytes read from standard input into a source file the ordinary +/// pipeline can process, or the skip that says why it cannot. Detection has no +/// path to work with, so it is driven by `--language` or by the contents. +/// +/// Declarative profiles and plugins route on a file extension, which standard +/// input does not have; a pipe is therefore always handled by a built-in +/// language or not at all. +pub fn stdin_source( + bytes: Vec, + resolved: &ResolvedConfig, + forced_language: Option, + forced_dialect: Option, +) -> Result { + let skipped = |reason: &str, error: bool| SkippedFile { + path: PathBuf::from(STDIN_PATH), + reason: reason.to_owned(), + error, + // Standard input was named on the command line, so its skip is always + // reported on its own line rather than folded into the summary. + explicit: true, + }; + if bytes.iter().take(8192).any(|byte| *byte == 0) { + return Err(skipped("binary file (NUL byte)", false)); + } + let detection = forced_language + .map(|language| Detection { + language, + dialect: forced_dialect.unwrap_or(Dialect::Standard), + reason: "command-line", + }) + .or_else(|| detect_language(None, &bytes)); + let Some(Detection { + language, dialect, .. + }) = detection + else { + return Err(skipped(STDIN_LANGUAGE_HELP, true)); + }; + if forced_language.is_none() + && resolved + .config + .languages + .get(language.as_str()) + .and_then(|item| item.enabled) + == Some(false) + { + return Err(skipped("language disabled by configuration", false)); + } + Ok(SourceFile { + path: PathBuf::from(STDIN_PATH), + source: bytes, + language, + dialect: forced_dialect.unwrap_or(dialect), + profile: None, + plugin: None, + }) +} + pub fn discover( paths: &[PathBuf], resolved: &ResolvedConfig, diff --git a/rust/ocomment/src/git.rs b/rust/ocomment/src/git.rs index f06831c..e848b2a 100644 --- a/rust/ocomment/src/git.rs +++ b/rust/ocomment/src/git.rs @@ -39,6 +39,9 @@ pub struct StagedRequest<'a> { pub presentation: Presentation, pub verbosity: Verbosity, pub preview: bool, + /// The run only previews the patch; `fix --dry-run` writes nothing to + /// the index and reports what a real run would remove. + pub dry_run: bool, } pub fn run_staged(request: StagedRequest<'_>) -> Result { @@ -54,6 +57,7 @@ pub fn run_staged(request: StagedRequest<'_>) -> Result { presentation, verbosity, preview, + dry_run, } = request; let root = repository_root()?; let names = staged_paths(&root, paths)?; @@ -194,7 +198,7 @@ pub fn run_staged(request: StagedRequest<'_>) -> Result { verbosity, preview, explain: false, - dry_run: false, + dry_run, force_invalid: resolved.config.policy.force_invalid, applied, }, @@ -424,7 +428,12 @@ fn hash_object(root: &Path, bytes: &[u8]) -> Result { .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; - child.stdin.take().expect("piped stdin").write_all(bytes)?; + child + .stdin + .take() + .expect("piped stdin") + .write_all(bytes) + .context("cannot write the rewritten blob to git hash-object")?; let output = child.wait_with_output()?; if !output.status.success() { bail!( diff --git a/rust/ocomment/src/main.rs b/rust/ocomment/src/main.rs index ac24b28..a6fd7e8 100644 --- a/rust/ocomment/src/main.rs +++ b/rust/ocomment/src/main.rs @@ -8,14 +8,84 @@ mod output; mod plugin; mod values; -use std::process::ExitCode; +use std::{ + io::{self, Write}, + process::ExitCode, +}; + +/// Whether the reader of the program's own output is what ended the run. +/// +/// `ocomment … | head` closes the pipe as soon as the reader has what it came +/// for. That is the reader finishing, not the run failing, so — following the +/// convention `rg` and `fd` set — the process ends quietly with status 0 +/// rather than reporting an I/O error to a terminal that may itself be gone. +/// The failing write can be several layers down: the serializer wraps it, and +/// the caller adds context on top. +/// +/// Only the writers of *our* report may claim this, and they say so by tagging +/// the failure with [`output::OutputPipeClosed`]. A bare `BrokenPipe` from +/// anywhere else — the write that feeds a rewritten blob to `git hash-object`, +/// above all — is a real failure whose silent success would lose data. +fn output_pipe_closed(error: &anyhow::Error) -> bool { + error + .chain() + .any(|cause| cause.downcast_ref::().is_some()) +} fn main() -> ExitCode { match cli::run() { Ok(code) => ExitCode::from(code), + Err(error) if output_pipe_closed(&error) => ExitCode::SUCCESS, Err(error) => { - eprintln!("ocomment: {error:#}"); + // Nothing is left to try if even the report cannot be written, and + // `eprintln!` would panic there — an abort under the release + // profile — so the failure of the last write is dropped. + let _ = writeln!(io::stderr(), "ocomment: {error:#}"); ExitCode::from(2) } } } + +#[cfg(test)] +mod tests { + use super::{output::OutputPipeClosed, output_pipe_closed}; + use anyhow::{Context, Result}; + use std::io::{Error, ErrorKind}; + + /// The tag survives however many layers of context are added on top of it. + #[test] + fn a_tagged_output_pipe_is_recognized_through_its_context() { + let error = Result::<()>::Err(anyhow::Error::new(OutputPipeClosed)) + .context("cannot write standard output") + .context("check failed") + .unwrap_err(); + assert!(output_pipe_closed(&error)); + } + + /// `git hash-object` exiting before it reads the blob raises a bare + /// `BrokenPipe` that no output writer tagged. Ending quietly there would + /// report a `fix --staged` that never happened. + #[test] + fn an_untagged_broken_pipe_is_not_an_output_pipe_closure() { + let error = Result::<()>::Err(Error::from(ErrorKind::BrokenPipe).into()) + .context("cannot write the rewritten blob to git hash-object") + .context("fix failed") + .unwrap_err(); + assert!(!output_pipe_closed(&error)); + } + + #[test] + fn another_io_failure_is_not_an_output_pipe_closure() { + let error = Result::<()>::Err(Error::from(ErrorKind::StorageFull).into()) + .context("cannot write standard output") + .unwrap_err(); + assert!(!output_pipe_closed(&error)); + } + + #[test] + fn an_error_carrying_no_io_failure_is_not_an_output_pipe_closure() { + assert!(!output_pipe_closed(&anyhow::anyhow!( + "plugin `x` is not locked" + ))); + } +} diff --git a/rust/ocomment/src/output.rs b/rust/ocomment/src/output.rs index 0fb99e5..df0511e 100644 --- a/rust/ocomment/src/output.rs +++ b/rust/ocomment/src/output.rs @@ -7,7 +7,7 @@ use serde_json::{Value, json}; use similar::{ChangeTag, TextDiff}; use std::{ collections::BTreeMap, - io::{self, Write}, + io::{self, BufWriter, Write}, path::{Path, PathBuf}, }; use unicode_width::UnicodeWidthChar; @@ -56,10 +56,11 @@ pub struct RenderOptions { pub verbosity: Verbosity, /// Human lines carry a one-line rendering of the comment text. pub preview: bool, - // Plumbed for the presentation work that follows; nothing reads them yet. + // Plumbed for the presentation work that follows; nothing reads it yet. #[allow(dead_code)] pub explain: bool, - #[allow(dead_code)] + /// The run is `fix --dry-run`: it produces the diff but speaks the + /// vocabulary of the `fix` it is standing in for. pub dry_run: bool, /// `--force-invalid` was in effect, so a file that fails to scan still had /// its provably safe edits applied. @@ -79,9 +80,13 @@ pub struct Summary { pub files_changed: usize, pub comments_removed: usize, pub invalid_files: usize, - /// Non-error skips, counted under a short stable label rather than the - /// raw reason, which can carry a configured byte limit. + /// Non-error skips met while walking, counted under a short stable label + /// rather than the raw reason, which can carry a configured byte limit. + /// A path named on the command line is deliberately absent: it already has + /// its own line on standard output and must not be counted twice. pub skipped_by_reason: BTreeMap, + /// Non-error skips whose path was named on the command line. + pub named_skips: usize, pub io_errors: usize, } @@ -111,6 +116,8 @@ impl Summary { for item in skipped { if item.error { summary.io_errors += 1; + } else if item.explicit { + summary.named_skips += 1; } else { *summary .skipped_by_reason @@ -148,12 +155,17 @@ fn skip_label(reason: &str) -> &str { } } +/// `1 file` / `2 files`: the count and its noun, pluralized by the regular +/// rule. Every noun the summary counts goes through this. +fn plural(count: usize, noun: &str) -> String { + format!("{count} {noun}{}", if count == 1 { "" } else { "s" }) +} + /// `1 comment` / `2 removable comments`: the noun is pluralized and an /// optional adjective is placed in front of it. fn comments(count: usize, adjective: &str) -> String { - let plural = if count == 1 { "" } else { "s" }; let space = if adjective.is_empty() { "" } else { " " }; - format!("{count} {adjective}{space}comment{plural}") + plural(count, &format!("{adjective}{space}comment")) } #[derive(Clone, Debug)] @@ -219,27 +231,53 @@ fn preview(source: &[u8], span: ByteSpan, max_columns: usize) -> String { truncate(folded, max_columns) } -/// C0, DEL, and C1. None of these may reach the terminal verbatim. +/// C0, DEL, C1, and the bidirectional and separator format controls. None of +/// these may reach the terminal verbatim: C0 drives it, the bidi overrides and +/// isolates can make a comment render as its own reverse, and U+2028/U+2029 +/// break the promise that a preview is one line. U+061C joins the marks it +/// belongs with, and U+FEFF is invisible wherever it lands. fn is_control(character: char) -> bool { - matches!(character, '\u{0}'..='\u{1f}' | '\u{7f}'..='\u{9f}') + matches!( + character, + '\u{0}'..='\u{1f}' + | '\u{7f}'..='\u{9f}' + | '\u{61c}' + | '\u{200e}'..='\u{200f}' + | '\u{2028}'..='\u{2029}' + | '\u{202a}'..='\u{202e}' + | '\u{2066}'..='\u{2069}' + | '\u{feff}' + ) } fn columns(character: char) -> usize { UnicodeWidthChar::width(character).unwrap_or(0) } -/// Cut `text` to `max_columns` display columns, never inside a wide character, -/// leaving room for the ellipsis that marks the cut. +/// How many characters a preview may carry for each column it may occupy. +/// Zero-width and combining characters cost no columns, so the width budget on +/// its own cannot bound the line a terminal has to hold. +const PREVIEW_CHARS_PER_COLUMN: usize = 4; + +/// Cut `text` to `max_columns` display columns and to a hard character cap, +/// never inside a wide character, leaving room for the ellipsis that marks the +/// cut. fn truncate(text: String, max_columns: usize) -> String { - if text.chars().map(columns).sum::() <= max_columns { + let max_chars = max_columns.saturating_mul(PREVIEW_CHARS_PER_COLUMN); + if text.chars().map(columns).sum::() <= max_columns && text.chars().count() <= max_chars + { return text; } - let budget = max_columns.saturating_sub(1); + let column_budget = max_columns.saturating_sub(1); + let char_budget = max_chars.saturating_sub(1); let mut cut = String::with_capacity(text.len()); let mut width = 0usize; - for character in text.chars() { + for (taken, character) in text.chars().enumerate() { + if taken >= char_budget { + break; + } width += columns(character); - if width > budget { + if width > column_budget { break; } cut.push(character); @@ -264,21 +302,105 @@ fn preview_suffix(source: &[u8], span: ByteSpan, options: &RenderOptions) -> Str ) } +/// The handle every path that writes the product of a run takes: standard +/// output, locked once for the whole run and buffered. +/// +/// `println!` panics when its write fails, and the release profile aborts on +/// panic, so a reader that stops early — `ocomment … | head` — would end the +/// process with SIGABRT. Writing through a handle that returns its errors lets +/// the caller decide instead, and `main` ends a closed pipe quietly. +pub type Stdout = BufWriter>; + +/// Lock standard output for the rest of the run and buffer it. +pub fn stdout() -> Stdout { + BufWriter::new(io::stdout().lock()) +} + +/// The reader of the program's own output went away mid-run. +/// +/// A broken pipe is only benign when it is *our* report that could not be +/// written; `ocomment … | head` is a reader that finished, not a run that +/// failed. Every other broken pipe — writing a rewritten blob into +/// `git hash-object`, for one — is a real failure, so the benign case is +/// tagged with this marker at the write that raised it instead of being +/// recognized by error kind anywhere in the chain. +#[derive(Debug)] +pub struct OutputPipeClosed; + +impl std::fmt::Display for OutputPipeClosed { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("the reader of standard output closed the pipe") + } +} + +impl std::error::Error for OutputPipeClosed {} + +/// Push the last buffered bytes out. +/// +/// A `BufWriter` drops the error of the write it performs while being dropped, +/// so every writer is finished by hand and the failure reaches the caller. +pub fn finish(writer: &mut impl Write) -> Result<()> { + wrote(writer.flush()) +} + +/// Raise one write to the program's own output, tagging the reader that closed +/// the pipe so `main` can end quietly for that case alone. +pub fn wrote(result: io::Result<()>) -> Result<()> { + result.map_err(output_failure) +} + +/// The error one failed write to our own output becomes. +fn output_failure(error: io::Error) -> anyhow::Error { + if error.kind() == io::ErrorKind::BrokenPipe { + return anyhow::Error::new(OutputPipeClosed); + } + anyhow::Error::new(error).context("cannot write standard output") +} + +/// Write one line of commentary to standard error. +/// +/// Commentary — the `-v` trace, the end-of-run summary — is not the product of +/// the run, so a reader that has already gone away is not a failure to report: +/// a closed pipe is dropped and only a real write failure is raised. What must +/// not happen is what `eprintln!` does, which is panic, and so abort under the +/// release profile. +pub fn note(writer: &mut impl Write, line: &str) -> Result<()> { + match writeln!(writer, "{line}") { + Err(error) if error.kind() != io::ErrorKind::BrokenPipe => { + Err(anyhow::Error::new(error).context("cannot write standard error")) + } + _ => Ok(()), + } +} + +/// Turn a serialization failure back into the I/O error it usually is. +/// +/// `serde_json` reports a failed write as an error of its own whose `source` +/// is the *source* of the I/O error rather than the I/O error itself, so a +/// closed pipe would be invisible to anything walking the chain. Its `From` +/// conversion hands the original error back. +fn write_error(error: serde_json::Error) -> anyhow::Error { + output_failure(io::Error::from(error)) +} + pub fn render( files: &[ProcessedFile], skipped: &[SkippedFile], options: &RenderOptions, ) -> Result<()> { + let mut output = stdout(); match options.format { - OutputFormat::Human => render_human(files, skipped, options), - OutputFormat::Json => render_json(files, skipped), - OutputFormat::Jsonl => render_jsonl(files, skipped), - OutputFormat::Sarif => render_sarif(files, skipped), - OutputFormat::Github => render_github(files, skipped), - } + OutputFormat::Human => render_human(&mut output, files, skipped, options), + OutputFormat::Json => render_json(&mut output, files, skipped), + OutputFormat::Jsonl => render_jsonl(&mut output, files, skipped), + OutputFormat::Sarif => render_sarif(&mut output, files, skipped), + OutputFormat::Github => render_github(&mut output, files, skipped), + }?; + finish(&mut output) } fn render_human( + output: &mut impl Write, files: &[ProcessedFile], skipped: &[SkippedFile], options: &RenderOptions, @@ -287,22 +409,20 @@ fn render_human( let presentation = options.presentation; let quiet = options.verbosity == Verbosity::Quiet; let verbose = options.verbosity == Verbosity::Verbose; - let stdout = io::stdout(); - let mut output = stdout.lock(); for file in files { if operation == Operation::Diff && file.source != file.result.output { - if !quiet { - write!( - output, - "{}", - unified_diff(&file.path, &file.source, &file.result.output) - )?; - } + // The patch is the product of `diff`, so `-q` keeps it and drops + // only the summary that follows on standard error. + wrote(write!( + output, + "{}", + unified_diff(&file.path, &file.source, &file.result.output) + ))?; continue; } for diagnostic in &file.result.report.diagnostics { let (line, column) = line_column(&file.source, diagnostic.span.start); - writeln!( + wrote(writeln!( output, "{}:{line}:{column}: {}{}[{}]{}: {}", display_path(&file.path, presentation.hyperlinks), @@ -311,15 +431,13 @@ fn render_human( diagnostic.code, color("\x1b[0m", presentation.color), diagnostic.message - )?; - } - if quiet { - continue; + ))?; } if operation == Operation::Scan { + // The listing is the product of `scan`; `-q` keeps it too. for comment in &file.result.report.comments { let (line, column) = line_column(&file.source, comment.span.start); - writeln!( + wrote(writeln!( output, "{}:{line}:{column}: {} {} {}..{}{}", display_path(&file.path, presentation.hyperlinks), @@ -328,16 +446,18 @@ fn render_human( comment.span.start, comment.span.end, preview_suffix(&file.source, comment.span, options) - )?; + ))?; } + } else if quiet { + continue; } else if operation == Operation::Fix { if options.applied && file.source != file.result.output { - writeln!( + wrote(writeln!( output, "fixed {}: removed {}", display_path(&file.path, presentation.hyperlinks), comments(removable_count(file), "") - )?; + ))?; } } else { for comment in file @@ -348,7 +468,7 @@ fn render_human( .filter(|comment| comment.disposition.is_remove()) { let (line, column) = line_column(&file.source, comment.span.start); - writeln!( + wrote(writeln!( output, "{}:{line}:{column}: {}{}{}{}", display_path(&file.path, presentation.hyperlinks), @@ -356,7 +476,7 @@ fn render_human( removable_label(comment.kind), color("\x1b[0m", presentation.color), preview_suffix(&file.source, comment.span, options) - )?; + ))?; } } } @@ -365,59 +485,114 @@ fn render_human( if !item.error && (quiet || !(item.explicit || verbose)) { continue; } - writeln!( + wrote(writeln!( output, "{}: {}: {}", display_path(&item.path, presentation.hyperlinks), if item.error { "error" } else { "skipped" }, item.reason - )?; + ))?; } } if quiet { return Ok(()); } + // The findings are on standard output and the summary that follows is on + // standard error; a terminal sees both, so the buffer is emptied first to + // keep the report in the order it was written. + finish(output)?; let summary = Summary::compute(files, skipped, operation); let folded = !verbose && skipped.iter().any(|item| !item.error && !item.explicit); let stderr = io::stderr(); let mut report = stderr.lock(); if verbose && let Some(line) = kind_breakdown(files, options) { - writeln!(report, "{line}")?; - } - writeln!( - report, - "{}{}", - summary_line(&summary, options), - skip_clause(&summary, folded) - )?; + note(&mut report, &line)?; + } + note(&mut report, &summary_report(&summary, options, folded))?; if summary.invalid_files > 0 && !options.force_invalid { - writeln!( - report, - "{} file(s) have invalid syntax; nothing was written for them \ - (use --force-invalid to apply known-safe edits).", - summary.invalid_files + let (verb, pronoun) = if summary.invalid_files == 1 { + ("has", "it") + } else { + ("have", "them") + }; + note( + &mut report, + &format!( + "{} {verb} invalid syntax; nothing was written for {pronoun} \ + (use --force-invalid to apply known-safe edits).", + plural(summary.invalid_files, "file") + ), )?; } Ok(()) } +/// The whole end-of-run summary: the verdict for the run, the folded skips, +/// and the I/O errors that were listed one by one above it. +fn summary_report(summary: &Summary, options: &RenderOptions, folded: bool) -> String { + let skips = skip_clause(summary, folded); + let nothing = nothing_to(options); + let mut report = if summary.files_scanned > 0 { + format!("{}{skips}", summary_line(summary, options)) + } else if !skips.is_empty() { + // Nothing was scanned, so the verdict would count zero files; what the + // run actually did was pass every candidate over. + format!("Nothing to {nothing}:{skips}") + } else if summary.named_skips > 0 { + format!("Nothing to {nothing}.") + } else { + summary_line(summary, options) + }; + if summary.io_errors > 0 { + report.push_str(&format!(" {}.", plural(summary.io_errors, "I/O error"))); + } + report +} + +/// The verb a run uses for the work it found nothing to do. `fix --dry-run` +/// borrows the vocabulary of the `fix` it is standing in for, as it does +/// everywhere else in the summary. +fn nothing_to(options: &RenderOptions) -> &'static str { + match options.operation { + Operation::Check => "check", + Operation::Fix => "fix", + Operation::Diff if options.dry_run => "fix", + Operation::Diff => "diff", + Operation::Scan => "scan", + } +} + /// The one-line verdict for the run, without the skipped-file clause. fn summary_line(summary: &Summary, options: &RenderOptions) -> String { - let scanned = summary.files_scanned; + let scanned = plural(summary.files_scanned, "file"); let found = || { format!( - "Found {} in {} file(s) ({scanned} files scanned).", + "Found {} in {} ({scanned} scanned).", comments(summary.removable_comments, "removable"), - summary.files_with_removable + plural(summary.files_with_removable, "file") ) }; match options.operation { + // `fix --dry-run` is the diff of a fix: it counts what a real run would + // take out and points back at the run that would write it. + Operation::Diff if options.dry_run => { + if summary.removable_comments == 0 { + return format!("Nothing to fix in {scanned}."); + } + format!( + "Would remove {} in {}. Rerun without --dry-run to apply.", + comments(summary.removable_comments, ""), + plural(summary.files_with_removable, "file") + ) + } Operation::Check | Operation::Diff => { if summary.removable_comments == 0 { - return format!("No removable comments in {scanned} file(s)."); + return format!("No removable comments in {scanned}."); } let next = if options.operation == Operation::Diff { - "apply" + "apply the patch" + } else if summary.removable_comments == 1 { + "remove it" } else { "remove them" }; @@ -426,12 +601,12 @@ fn summary_line(summary: &Summary, options: &RenderOptions) -> String { Operation::Fix => { if options.applied && summary.files_changed > 0 { format!( - "Removed {} in {} file(s) ({scanned} files scanned).", + "Removed {} in {} ({scanned} scanned).", comments(summary.comments_removed, ""), - summary.files_changed + plural(summary.files_changed, "file") ) } else if summary.removable_comments == 0 { - format!("Nothing to fix in {scanned} file(s).") + format!("Nothing to fix in {scanned}.") } else { // The transaction never reached the disk; report what is still // there rather than claiming a removal. @@ -439,7 +614,7 @@ fn summary_line(summary: &Summary, options: &RenderOptions) -> String { } } Operation::Scan => format!( - "Scanned {scanned} file(s): {} ({} removable, {} kept).", + "Scanned {scanned}: {} ({} removable, {} kept).", comments(summary.removable_comments + summary.kept_comments, ""), summary.removable_comments, summary.kept_comments @@ -447,7 +622,9 @@ fn summary_line(summary: &Summary, options: &RenderOptions) -> String { } } -/// The skipped-file clause appended to the summary line. +/// The skipped-file clause appended to the summary line. Only the skips met +/// while walking are folded here; a named path was already reported on its own +/// line. fn skip_clause(summary: &Summary, folded: bool) -> String { let total = summary.skipped_files(); if total == 0 { @@ -459,7 +636,11 @@ fn skip_clause(summary: &Summary, folded: bool) -> String { .map(|(label, count)| format!("{label}: {count}")) .collect(); let hint = if folded { "; use -v to list" } else { "" }; - format!(" {total} file(s) skipped ({}{hint}).", reasons.join(", ")) + format!( + " {} skipped ({}{hint}).", + plural(total, "file"), + reasons.join(", ") + ) } /// The `-v` breakdown of what each comment kind contributed. @@ -514,7 +695,11 @@ fn display_path(path: &Path, hyperlinks: bool) -> String { format!("\x1b]8;;file://{target}\x1b\\{display}\x1b]8;;\x1b\\") } -fn render_json(files: &[ProcessedFile], skipped: &[SkippedFile]) -> Result<()> { +fn render_json( + output: &mut impl Write, + files: &[ProcessedFile], + skipped: &[SkippedFile], +) -> Result<()> { let values: Vec<_> = files.iter().map(json_file).collect(); let skipped: Vec<_> = skipped .iter() @@ -523,26 +708,30 @@ fn render_json(files: &[ProcessedFile], skipped: &[SkippedFile]) -> Result<()> { }) .collect(); serde_json::to_writer_pretty( - io::stdout().lock(), + &mut *output, &json!({"version": 1, "files": values, "skipped": skipped}), - )?; - println!(); + ) + .map_err(write_error)?; + wrote(writeln!(output))?; Ok(()) } -fn render_jsonl(files: &[ProcessedFile], skipped: &[SkippedFile]) -> Result<()> { - let stdout = io::stdout(); - let mut output = stdout.lock(); +fn render_jsonl( + output: &mut impl Write, + files: &[ProcessedFile], + skipped: &[SkippedFile], +) -> Result<()> { for file in files { - serde_json::to_writer(&mut output, &json_file(file))?; - writeln!(output)?; + serde_json::to_writer(&mut *output, &json_file(file)).map_err(write_error)?; + wrote(writeln!(output))?; } for item in skipped { serde_json::to_writer( - &mut output, + &mut *output, &json!({"type": "skip", "path": item.path.to_string_lossy(), "reason": item.reason, "error": item.error}), - )?; - writeln!(output)?; + ) + .map_err(write_error)?; + wrote(writeln!(output))?; } Ok(()) } @@ -558,7 +747,11 @@ fn json_file(file: &ProcessedFile) -> JsonFile<'_> { } } -fn render_sarif(files: &[ProcessedFile], skipped: &[SkippedFile]) -> Result<()> { +fn render_sarif( + output: &mut impl Write, + files: &[ProcessedFile], + skipped: &[SkippedFile], +) -> Result<()> { let mut results = Vec::new(); for file in files { for comment in file @@ -627,8 +820,8 @@ fn render_sarif(files: &[ProcessedFile], skipped: &[SkippedFile]) -> Result<()> "$schema": "https://json.schemastore.org/sarif-2.1.0.json", "runs": [{"tool": {"driver": {"name": "ocomment", "informationUri": "https://github.com/P4suta/OComment"}}, "results": results}] }); - serde_json::to_writer_pretty(io::stdout().lock(), &sarif)?; - println!(); + serde_json::to_writer_pretty(&mut *output, &sarif).map_err(write_error)?; + wrote(writeln!(output))?; Ok(()) } @@ -641,7 +834,11 @@ fn replacement_for_span(file: &ProcessedFile, span: ByteSpan) -> String { .unwrap_or_default() } -fn render_github(files: &[ProcessedFile], skipped: &[SkippedFile]) -> Result<()> { +fn render_github( + output: &mut impl Write, + files: &[ProcessedFile], + skipped: &[SkippedFile], +) -> Result<()> { for file in files { for comment in file .result @@ -651,24 +848,27 @@ fn render_github(files: &[ProcessedFile], skipped: &[SkippedFile]) -> Result<()> .filter(|comment| comment.disposition.is_remove()) { let (line, column) = line_column(&file.source, comment.span.start); - println!( + wrote(writeln!( + output, "::notice file={},line={line},col={column}::{}", github_escape(&file.path.to_string_lossy()), removable_label(comment.kind) - ); + ))?; } for diagnostic in &file.result.report.diagnostics { let (line, column) = line_column(&file.source, diagnostic.span.start); - println!( + wrote(writeln!( + output, "::error file={},line={line},col={column},title={}::{}", github_escape(&file.path.to_string_lossy()), github_escape(&diagnostic.code), github_escape(&diagnostic.message) - ); + ))?; } } for item in skipped { - println!( + wrote(writeln!( + output, "::{} file={},title={}::{}", if item.error { "error" } else { "notice" }, github_escape(&item.path.to_string_lossy()), @@ -678,7 +878,7 @@ fn render_github(files: &[ProcessedFile], skipped: &[SkippedFile]) -> Result<()> "OComment skipped file" }, github_escape(&item.reason) - ); + ))?; } Ok(()) } @@ -809,6 +1009,44 @@ mod tests { ); } + /// Bidi overrides and isolates can make a comment render as its own + /// reverse, and the line/paragraph separators break the one-line promise. + #[test] + fn preview_replaces_bidirectional_and_separator_controls() { + let source = "// \u{202e}reverse\u{202c} \u{200e}\u{200f} \u{2066}iso\u{2069} \ + \u{2028}\u{2029} \u{61c}\u{feff} end"; + assert_eq!( + preview_of(source.as_bytes(), 72), + "// \u{fffd}reverse\u{fffd} \u{fffd}\u{fffd} \u{fffd}iso\u{fffd} \ + \u{fffd}\u{fffd} \u{fffd}\u{fffd} end" + ); + for character in [ + '\u{61c}', '\u{200e}', '\u{200f}', '\u{202a}', '\u{202b}', '\u{202c}', '\u{202d}', + '\u{202e}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{2069}', '\u{2028}', '\u{2029}', + '\u{feff}', + ] { + assert!( + is_control(character), + "U+{:04X} still reaches the terminal", + character as u32 + ); + } + } + + /// Zero-width characters cost no display columns, so the width budget alone + /// cannot bound the line; a hard character cap must. + #[test] + fn preview_caps_the_character_count_of_a_zero_width_run() { + let source = format!("a{}", "\u{301}".repeat(1000)); + let rendered = preview_of(source.as_bytes(), 8); + assert!( + rendered.chars().count() <= 8 * 4, + "preview is {} characters wide", + rendered.chars().count() + ); + assert!(rendered.ends_with('\u{2026}'), "truncation is unmarked"); + } + #[test] fn preview_reads_only_the_span() { let source = b"let x = 1; // TODO remove\n"; diff --git a/rust/ocomment/src/plugin.rs b/rust/ocomment/src/plugin.rs index 76f8a44..492b409 100644 --- a/rust/ocomment/src/plugin.rs +++ b/rust/ocomment/src/plugin.rs @@ -1,4 +1,4 @@ -use crate::config::PluginsConfig; +use crate::{config::PluginsConfig, output::wrote}; use anyhow::{Context, Result, anyhow, bail, ensure}; use ocomment_core::{ ByteSpan, CommentKind, Language, TransformOptions, TransformResult, transform_spans, @@ -519,16 +519,26 @@ fn locked_artifact_candidate(root: &Path, artifact: &str) -> Result { } pub fn add( + output: &mut impl Write, root: &Path, source: &str, requested_name: Option<&str>, expected: Option<&str>, identity: Option<&str>, ) -> Result<()> { - install(root, source, requested_name, expected, identity, true) + install( + output, + root, + source, + requested_name, + expected, + identity, + true, + ) } fn install( + output: &mut impl Write, root: &Path, source: &str, requested_name: Option<&str>, @@ -599,11 +609,11 @@ fn install( }, ); save_lock(root, &lock)?; - println!("added plugin {name}"); + wrote(writeln!(output, "added plugin {name}"))?; Ok(()) } -pub fn remove(root: &Path, name: &str) -> Result<()> { +pub fn remove(output: &mut impl Write, root: &Path, name: &str) -> Result<()> { let mut lock = load_lock(root)?; let removed = lock .plugins @@ -623,25 +633,26 @@ pub fn remove(root: &Path, name: &str) -> Result<()> { } } save_lock(root, &lock)?; - println!("removed plugin {name}"); + wrote(writeln!(output, "removed plugin {name}"))?; Ok(()) } -pub fn list(root: &Path) -> Result<()> { +pub fn list(output: &mut impl Write, root: &Path) -> Result<()> { let lock = load_lock(root)?; if lock.plugins.is_empty() { - println!("no plugins locked"); + wrote(writeln!(output, "no plugins locked"))?; } for (name, plugin) in lock.plugins { - println!( + wrote(writeln!( + output, "{name}\t{}\tsha256:{}\tAPI {}", plugin.version, plugin.sha256, plugin.api - ); + ))?; } Ok(()) } -pub fn verify(root: &Path, selected: Option<&str>) -> Result<()> { +pub fn verify(output: &mut impl Write, root: &Path, selected: Option<&str>) -> Result<()> { let lock = load_lock(root)?; for (name, plugin) in lock .plugins @@ -657,7 +668,11 @@ pub fn verify(root: &Path, selected: Option<&str>) -> Result<()> { if actual != plugin.sha256 { bail!("plugin `{name}` digest mismatch"); } - println!("plugin {name}: verified sha256:{}", plugin.sha256); + wrote(writeln!( + output, + "plugin {name}: verified sha256:{}", + plugin.sha256 + ))?; } if let Some(name) = selected && !lock.plugins.contains_key(name) @@ -665,12 +680,12 @@ pub fn verify(root: &Path, selected: Option<&str>) -> Result<()> { bail!("plugin `{name}` is not locked"); } if lock.plugins.is_empty() { - println!("plugins: none (offline lock is valid)"); + wrote(writeln!(output, "plugins: none (offline lock is valid)"))?; } Ok(()) } -pub fn update(root: &Path, selected: Option<&str>) -> Result<()> { +pub fn update(output: &mut impl Write, root: &Path, selected: Option<&str>) -> Result<()> { let lock = load_lock(root)?; let entries: Vec<_> = lock .plugins @@ -683,11 +698,12 @@ pub fn update(root: &Path, selected: Option<&str>) -> Result<()> { } for (name, plugin) in entries { if !is_remote_source(&plugin.source) { - add(root, &plugin.source, Some(&name), None, None)?; + add(output, root, &plugin.source, Some(&name), None, None)?; } else { // The existing signature identity authorizes a freshly fetched // artifact. Its new digest is then written to the lockfile. install( + output, root, &plugin.source, Some(&name), @@ -704,7 +720,7 @@ fn is_remote_source(source: &str) -> bool { source.starts_with("https://") || source.starts_with("gh:") || source.starts_with("oci:") } -pub fn new_plugin(path: &Path) -> Result<()> { +pub fn new_plugin(output: &mut impl Write, path: &Path) -> Result<()> { fs::create_dir(path) .with_context(|| format!("refusing to overwrite plugin directory {}", path.display()))?; fs::create_dir(path.join("src"))?; @@ -790,7 +806,11 @@ The host provides no WASI, filesystem, network, clock, or random imports. Keep t self-contained and return sorted, non-overlapping, non-empty byte spans. "#, )?; - println!("created plugin scaffold {}", path.display()); + wrote(writeln!( + output, + "created plugin scaffold {}", + path.display() + ))?; Ok(()) } diff --git a/rust/ocomment/tests/cli.rs b/rust/ocomment/tests/cli.rs index 4f5a138..7b20dfd 100644 --- a/rust/ocomment/tests/cli.rs +++ b/rust/ocomment/tests/cli.rs @@ -1,7 +1,8 @@ use std::{ fs, + io::{Read, Write}, path::Path, - process::{Command, Output}, + process::{Command, ExitStatus, Output, Stdio}, }; use tempfile::TempDir; @@ -18,6 +19,26 @@ fn run(directory: &Path, arguments: &[&str]) -> Output { .unwrap() } +/// Run the binary with `input` piped to its standard input. +fn run_stdin(directory: &Path, arguments: &[&str], input: &[u8]) -> Output { + let mut child = Command::new(binary()) + .current_dir(directory) + .env("PATH", "/usr/bin:/bin") + .args(arguments) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child + .stdin + .take() + .expect("standard input was piped") + .write_all(input) + .unwrap(); + child.wait_with_output().unwrap() +} + fn git(directory: &Path, arguments: &[&str]) -> Vec { let output = Command::new("/usr/bin/git") .current_dir(directory) @@ -937,8 +958,8 @@ fn check_writes_its_summary_to_standard_error() { ); assert_eq!( stderr, - "Found 1 removable comment in 1 file(s) (1 files scanned). \ - Run `ocomment fix` to remove them.\n" + "Found 1 removable comment in 1 file (1 file scanned). \ + Run `ocomment fix` to remove it.\n" ); } @@ -950,7 +971,7 @@ fn a_clean_check_summarizes_the_files_it_scanned() { assert_eq!(output.status.code(), Some(0)); assert_eq!( String::from_utf8(output.stderr).unwrap(), - "No removable comments in 1 file(s).\n" + "No removable comments in 1 file.\n" ); assert_eq!(String::from_utf8(output.stdout).unwrap(), ""); } @@ -974,8 +995,8 @@ fn diff_keeps_the_patch_on_stdout_and_summarizes_on_stderr() { ); assert_eq!( String::from_utf8(output.stderr).unwrap(), - "Found 1 removable comment in 1 file(s) (1 files scanned). \ - Run `ocomment fix` to apply.\n" + "Found 1 removable comment in 1 file (1 file scanned). \ + Run `ocomment fix` to apply the patch.\n" ); } @@ -1000,7 +1021,7 @@ fn fix_reports_every_changed_file_and_summarizes_on_stderr() { ); assert_eq!( String::from_utf8(output.stderr).unwrap(), - "Removed 1 comment in 1 file(s) (1 files scanned).\n" + "Removed 1 comment in 1 file (1 file scanned).\n" ); } @@ -1013,7 +1034,7 @@ fn a_clean_fix_says_there_was_nothing_to_do() { assert_eq!(String::from_utf8(output.stdout).unwrap(), ""); assert_eq!( String::from_utf8(output.stderr).unwrap(), - "Nothing to fix in 1 file(s).\n" + "Nothing to fix in 1 file.\n" ); } @@ -1029,7 +1050,7 @@ fn scan_summarizes_the_comment_counts() { assert_eq!(output.status.code(), Some(0)); assert_eq!( String::from_utf8(output.stderr).unwrap(), - "Scanned 1 file(s): 2 comments (1 removable, 1 kept).\n" + "Scanned 1 file: 2 comments (1 removable, 1 kept).\n" ); } @@ -1100,7 +1121,7 @@ fn directory_walks_fold_skipped_files_into_the_summary() { "a walked skip was listed individually:\n{stdout}" ); assert!( - stderr.contains("1 file(s) skipped (unknown language: 1; use -v to list)."), + stderr.contains("1 file skipped (unknown language: 1; use -v to list)."), "summary is:\n{stderr}" ); } @@ -1147,7 +1168,15 @@ fn machine_formats_never_emit_the_summary() { for format in ["json", "jsonl", "sarif", "github"] { let output = run( directory.path(), - &["check", "sample.rs", "--format", format], + &[ + "check", + "sample.rs", + "-v", + "--progress", + "always", + "--format", + format, + ], ); assert_eq!( String::from_utf8(output.stderr).unwrap(), @@ -1176,7 +1205,7 @@ fn a_blocked_fix_does_not_claim_removals() { "fix claimed a write that was blocked:\n{stdout}" ); assert!( - stderr.contains("1 file(s) have invalid syntax; nothing was written for them (use --force-invalid to apply known-safe edits)."), + stderr.contains("1 file has invalid syntax; nothing was written for it (use --force-invalid to apply known-safe edits)."), "summary is:\n{stderr}" ); assert!( @@ -1198,13 +1227,13 @@ fn staged_runs_also_emit_the_summary() { assert_eq!(output.status.code(), Some(1)); assert_eq!( String::from_utf8(output.stderr).unwrap(), - "Found 1 removable comment in 1 file(s) (1 files scanned). \ - Run `ocomment fix` to remove them.\n" + "Found 1 removable comment in 1 file (1 file scanned). \ + Run `ocomment fix` to remove it.\n" ); } #[test] -fn help_lists_the_verbosity_flags_and_retires_the_progress_indicator() { +fn help_lists_the_verbosity_and_progress_flags() { let directory = tempfile::tempdir().unwrap(); let output = run(directory.path(), &["check", "--help"]); assert_eq!(output.status.code(), Some(0)); @@ -1221,24 +1250,63 @@ fn help_lists_the_verbosity_flags_and_retires_the_progress_indicator() { ); } -/// `--progress` stays accepted, but the summary replaced the line it drew. -#[test] -fn the_progress_flag_no_longer_prints_a_progress_line() { +/// Fill a directory with `count` one-comment Rust files. +fn many_files(count: usize) -> TempDir { let directory = tempfile::tempdir().unwrap(); - fs::write( - directory.path().join("sample.rs"), - b"let x = 1; // remove\n", - ) - .unwrap(); - let output = run( - directory.path(), - &["check", "--progress", "always", "sample.rs"], + for index in 0..count { + fs::write( + directory.path().join(format!("file{index:03}.rs")), + b"let x = 1; // remove\n", + ) + .unwrap(); + } + directory +} + +/// `--progress always` draws a live counter on standard error and still leaves +/// the end-of-run summary readable once the counter line is cleared. +#[test] +fn progress_always_draws_a_live_counter_and_keeps_the_summary() { + let directory = many_files(120); + let output = run(directory.path(), &["check", "--progress", "always", "."]); + assert_eq!(output.status.code(), Some(1)); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!( + stderr.contains("ocomment: scanning 120/120 files"), + "progress counter is missing from:\n{stderr:?}" + ); + assert!( + stderr.contains("\r\x1b[2K"), + "the counter line was never cleared:\n{stderr:?}" ); + assert!( + stderr.ends_with( + "Found 120 removable comments in 120 files (120 files scanned). \ + Run `ocomment fix` to remove them.\n" + ), + "summary is missing from:\n{stderr:?}" + ); +} + +#[test] +fn progress_never_draws_nothing_and_quiet_wins_over_progress() { + let directory = many_files(120); + let output = run(directory.path(), &["check", "--progress", "never", "."]); assert_eq!(output.status.code(), Some(1)); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!( + !stderr.contains("scanning"), + "`--progress never` still drew a counter:\n{stderr:?}" + ); + let quiet = run( + directory.path(), + &["check", "-q", "--progress", "always", "."], + ); + assert_eq!(quiet.status.code(), Some(1)); assert_eq!( - String::from_utf8(output.stderr).unwrap(), - "Found 1 removable comment in 1 file(s) (1 files scanned). \ - Run `ocomment fix` to remove them.\n" + String::from_utf8(quiet.stderr).unwrap(), + "", + "`-q` did not silence the progress counter" ); } @@ -1354,3 +1422,743 @@ fn help_documents_the_preview_switch() { "`check --help` lacks --no-preview:\n{help}" ); } + +/// `clap_mangen` dumps `after_long_help` as one opaque `.SH EXTRA` blob; the +/// manual must carry the same content as real roff sections instead. +#[test] +fn man_page_renders_real_sections_instead_of_one_extra_blob() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["man"]); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let page = String::from_utf8(output.stdout).unwrap(); + for needle in [ + ".SH EXIT STATUS", + ".SH FILES", + ".SH EXAMPLES", + ".SH SEE ALSO", + ] { + assert!(page.contains(needle), "man page lacks {needle}:\n{page}"); + } + assert!( + !page.contains(".SH EXTRA"), + "the help blob is still dumped verbatim:\n{page}" + ); +} + +/// A bidirectional override can make a comment render as its own reverse; the +/// preview must neutralize the whole format-control class, not only C0. +#[test] +fn a_previewed_comment_cannot_reorder_the_line_with_bidi_controls() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("bidi.rs"), + "let x = 1; // \u{202e}drowssap\u{202c} end\n".as_bytes(), + ) + .unwrap(); + let output = run(directory.path(), &["check", "bidi.rs"]); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + !stdout.contains('\u{202e}'), + "a bidi override reached the terminal:\n{stdout:?}" + ); + assert_eq!( + stdout, + "bidi.rs:1:12: removable line comment: // \u{fffd}drowssap\u{fffd} end\n" + ); +} + +/// An explicitly named skip already has its own line on standard output, so +/// the folded clause must not count it a second time. +#[test] +fn a_named_skip_is_not_counted_twice_in_the_summary() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + fs::write(directory.path().join("notes.md"), b"# notes\n").unwrap(); + let output = run(directory.path(), &["check", "sample.rs", "notes.md"]); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!( + stdout.contains("notes.md: skipped: unknown language"), + "check output is:\n{stdout}" + ); + assert_eq!( + stderr, + "Found 1 removable comment in 1 file (1 file scanned). \ + Run `ocomment fix` to remove it.\n" + ); +} + +/// Scanning nothing at all is not "no removable comments in 0 files": say what +/// actually happened to the files that were passed over. +#[test] +fn a_run_that_scans_nothing_reports_the_skips_instead() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("notes.md"), b"# notes\n").unwrap(); + let output = run(directory.path(), &["check", "."]); + assert_eq!(output.status.code(), Some(0)); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "Nothing to check: 1 file skipped (unknown language: 1; use -v to list).\n" + ); +} + +/// Every noun in the summary is pluralized; `file(s)` never reaches a user. +#[test] +fn the_summary_pluralizes_every_noun() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("a.rs"), + b"let x = 1; // one\nlet y = 2; // two\n", + ) + .unwrap(); + fs::write(directory.path().join("b.rs"), b"let z = 3; // three\n").unwrap(); + let output = run(directory.path(), &["check", "a.rs", "b.rs"]); + assert_eq!(output.status.code(), Some(1)); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert_eq!( + stderr, + "Found 3 removable comments in 2 files (2 files scanned). \ + Run `ocomment fix` to remove them.\n" + ); + assert!(!stderr.contains("(s)"), "summary is:\n{stderr}"); +} + +/// An unreadable path is an I/O error, and the summary must own up to it. +#[cfg(unix)] +#[test] +fn the_summary_counts_io_errors() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["check", "sample.rs", "missing.rs"]); + assert_eq!(output.status.code(), Some(2)); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!( + stderr.contains("1 I/O error."), + "the summary hides the I/O error:\n{stderr}" + ); +} + +/// `-q` silences the chatter, never the product: a patch is the whole point of +/// `diff`, so it survives. +#[test] +fn quiet_diff_still_writes_the_patch() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["diff", "-q", "sample.rs"]); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.starts_with("--- a/sample.rs"), "diff is:\n{stdout}"); + assert!( + stdout.contains("-let x = 1; // remove"), + "diff is:\n{stdout}" + ); + assert_eq!(String::from_utf8(output.stderr).unwrap(), ""); +} + +/// The same rule for `scan`: the listing is the product. +#[test] +fn quiet_scan_still_writes_the_listing() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("a.py"), + b"#!/usr/bin/env python3\nx = 1 # remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["scan", "-q", "a.py"]); + assert_eq!(output.status.code(), Some(0)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert_eq!(stdout.lines().count(), 2, "scan output is:\n{stdout}"); + assert!( + stdout.contains("a.py:2:8: line remove "), + "scan output is:\n{stdout}" + ); + assert_eq!(String::from_utf8(output.stderr).unwrap(), ""); +} + +/// Nothing was scanned and the only skip was named on the command line, where +/// it already has its own line: the summary says so without repeating it. +#[test] +fn a_run_of_only_named_skips_does_not_repeat_them() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("notes.md"), b"# notes\n").unwrap(); + let output = run(directory.path(), &["check", "notes.md"]); + assert_eq!(output.status.code(), Some(0)); + assert!( + String::from_utf8(output.stdout) + .unwrap() + .contains("notes.md: skipped: unknown language") + ); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "Nothing to check.\n" + ); +} + +/// `-` in the PATH list is standard input: it is scanned like any other file +/// and reported under the pseudo path ``. +#[test] +fn a_dash_reads_standard_input_as_a_file() { + let directory = tempfile::tempdir().unwrap(); + let output = run_stdin( + directory.path(), + &["check", "--language", "rust", "-"], + b"let x = 1; // note\n", + ); + assert_eq!( + output.status.code(), + Some(1), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + String::from_utf8(output.stdout).unwrap(), + ":1:12: removable line comment: // note\n" + ); +} + +/// The patch for standard input names the same pseudo path. +#[test] +fn a_dash_diffs_standard_input_under_the_pseudo_path() { + let directory = tempfile::tempdir().unwrap(); + let output = run_stdin( + directory.path(), + &["diff", "--language", "rust", "-"], + b"let x = 1; // note\n", + ); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.starts_with("--- a/\n"), "diff is:\n{stdout}"); + assert!(stdout.contains("-let x = 1; // note"), "diff is:\n{stdout}"); +} + +/// The machine formats carry the pseudo path too, so a piped run is as +/// scriptable as a walked one. +#[test] +fn a_dash_names_standard_input_in_json() { + let directory = tempfile::tempdir().unwrap(); + let output = run_stdin( + directory.path(), + &["check", "--format", "json", "--language", "rust", "-"], + b"let x = 1; // note\n", + ); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains("\"path\": \"\""), + "json is:\n{stdout}" + ); +} + +/// Standard input has no name to detect a language from, so bytes that carry +/// no signature are a usage error with an actionable message. +#[test] +fn undetectable_standard_input_asks_for_a_language() { + let directory = tempfile::tempdir().unwrap(); + let output = run_stdin(directory.path(), &["check", "-"], b"let x = 1; // note\n"); + assert_eq!(output.status.code(), Some(2)); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "ocomment: cannot detect the language of standard input; \ + pass --language (see `ocomment languages`)\n" + ); +} + +/// `strip` and `check -` read the same standard input, so they must fail with +/// the same words when they cannot tell what it is. +#[test] +fn strip_and_check_agree_on_the_undetectable_input_message() { + let directory = tempfile::tempdir().unwrap(); + let stripped = run_stdin(directory.path(), &["strip"], b"let x = 1; // note\n"); + let checked = run_stdin(directory.path(), &["check", "-"], b"let x = 1; // note\n"); + assert_eq!(stripped.status.code(), Some(2)); + assert_eq!(checked.status.code(), Some(2)); + assert_eq!( + String::from_utf8(stripped.stderr).unwrap(), + String::from_utf8(checked.stderr).unwrap() + ); +} + +/// A pipe cannot be rewritten in place; `fix` says so and names the command +/// that does write a stripped stream. +#[test] +fn fix_refuses_standard_input() { + let directory = tempfile::tempdir().unwrap(); + let output = run_stdin( + directory.path(), + &["fix", "--language", "rust", "-"], + b"let x = 1; // note\n", + ); + assert_eq!(output.status.code(), Some(2)); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "ocomment: cannot rewrite standard input in place; use `ocomment strip`\n" + ); +} + +/// There is only one standard input, so naming it twice is a usage error +/// rather than a silently deduplicated target. +#[test] +fn standard_input_may_be_named_only_once() { + let directory = tempfile::tempdir().unwrap(); + let output = run_stdin( + directory.path(), + &["check", "--language", "rust", "-", "-"], + b"let x = 1; // note\n", + ); + assert_eq!(output.status.code(), Some(2)); + assert!( + String::from_utf8(output.stderr) + .unwrap() + .contains("cannot read standard input twice"), + "the second `-` was accepted" + ); +} + +/// `--staged` reads the Git index; a pipe cannot be one of its entries. +#[test] +fn standard_input_conflicts_with_staged() { + let directory = repository(); + let output = run_stdin( + directory.path(), + &["check", "--staged", "--language", "rust", "-"], + b"let x = 1; // note\n", + ); + assert_eq!(output.status.code(), Some(2)); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "ocomment: cannot read standard input with --staged; the index is the source\n" + ); +} + +/// `fix --dry-run` is `diff` with fix vocabulary: the patch goes to standard +/// output, the file keeps every byte, and the exit code still reports a +/// pending change. +#[test] +fn fix_dry_run_writes_a_patch_and_leaves_the_file_alone() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("sample.rs"); + let before = b"let x = 1; // remove\n"; + fs::write(&path, before).unwrap(); + + let output = run(directory.path(), &["fix", "--dry-run", "sample.rs"]); + assert_eq!(output.status.code(), Some(1)); + assert_eq!(fs::read(&path).unwrap(), before); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.starts_with("--- a/sample.rs\n"), + "diff is:\n{stdout}" + ); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "Would remove 1 comment in 1 file. Rerun without --dry-run to apply.\n" + ); +} + +/// With nothing to take out, the preview says what a real `fix` would say. +#[test] +fn fix_dry_run_on_a_clean_file_reports_nothing_to_fix() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("clean.rs"); + fs::write(&path, b"let x = 1;\n").unwrap(); + + let output = run(directory.path(), &["fix", "--dry-run", "clean.rs"]); + assert_eq!(output.status.code(), Some(0)); + assert_eq!(String::from_utf8(output.stdout).unwrap(), ""); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "Nothing to fix in 1 file.\n" + ); +} + +/// Both new entry points are discoverable from `--help`. +#[test] +fn help_documents_standard_input_and_the_dry_run() { + let directory = tempfile::tempdir().unwrap(); + let checked = run(directory.path(), &["check", "--help"]); + assert_eq!(checked.status.code(), Some(0)); + let help = String::from_utf8(checked.stdout).unwrap(); + assert!( + help.contains("`-` reads standard input"), + "`check --help` does not document `-`:\n{help}" + ); + let fixed = run(directory.path(), &["fix", "--help"]); + assert_eq!(fixed.status.code(), Some(0)); + let help = String::from_utf8(fixed.stdout).unwrap(); + assert!( + help.contains("--dry-run"), + "`fix --help` does not document --dry-run:\n{help}" + ); +} + +/// Standard input is one target among others, not a mode: a piped file and a +/// named one are reported by the same run. +#[test] +fn a_dash_can_be_mixed_with_named_paths() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("sample.rs"), b"let y = 2; // named\n").unwrap(); + let output = run_stdin( + directory.path(), + &["check", "--language", "rust", "sample.rs", "-"], + b"let x = 1; // piped\n", + ); + assert_eq!(output.status.code(), Some(1)); + assert_eq!( + String::from_utf8(output.stdout).unwrap(), + ":1:12: removable line comment: // piped\n\ + sample.rs:1:12: removable line comment: // named\n" + ); +} + +/// The default command takes the same PATH list, so `-` works without naming +/// `check` at all. +#[test] +fn the_default_command_also_reads_a_dash() { + let directory = tempfile::tempdir().unwrap(); + let output = run_stdin( + directory.path(), + &["--language", "rust", "-"], + b"let x = 1; // note\n", + ); + assert_eq!(output.status.code(), Some(1)); + assert_eq!( + String::from_utf8(output.stdout).unwrap(), + ":1:12: removable line comment: // note\n" + ); +} + +/// `--dry-run` previews the staged run too: the patch is the one `--staged` +/// would apply, and the index keeps every byte. +#[test] +fn fix_dry_run_previews_the_staged_patch_without_writing_the_index() { + let directory = repository(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + git(directory.path(), &["add", "sample.rs"]); + let output = run(directory.path(), &["fix", "--dry-run", "--staged"]); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.starts_with("--- a/sample.rs\n"), + "diff is:\n{stdout}" + ); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "Would remove 1 comment in 1 file. Rerun without --dry-run to apply.\n" + ); + assert_eq!( + git(directory.path(), &["show", ":sample.rs"]), + b"let x = 1; // remove\n" + ); +} + +/// A tree whose report is far larger than any pipe buffer, so a reader that +/// stops early is guaranteed to close the pipe while the run is still writing. +fn wide_tree(files: usize, comments: usize) -> TempDir { + let directory = tempfile::tempdir().unwrap(); + let mut source = String::new(); + for index in 0..comments { + source.push_str(&format!("let value{index} = {index}; // remove {index}\n")); + } + for index in 0..files { + fs::write(directory.path().join(format!("file{index}.rs")), &source).unwrap(); + } + directory +} + +/// Run the binary, take `head` bytes of its output, then close the pipe and +/// report how the run ended and what it said on standard error. +fn run_closed_pipe(directory: &Path, arguments: &[&str], head: usize) -> (ExitStatus, String) { + let mut child = Command::new(binary()) + .current_dir(directory) + .env("PATH", "/usr/bin:/bin") + .args(arguments) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let mut output = child.stdout.take().expect("standard output was piped"); + let mut taken = vec![0u8; head]; + if head > 0 { + output.read_exact(&mut taken).unwrap(); + } + // The reader has what it wanted; from here every write the run attempts + // fails with EPIPE. + drop(output); + let mut message = String::new(); + child + .stderr + .take() + .expect("standard error was piped") + .read_to_string(&mut message) + .unwrap(); + (child.wait().unwrap(), message) +} + +/// `ocomment check --format json . | head` is a reader that stops early, not a +/// failure: the run ends quietly with status 0 and says nothing. +#[test] +fn a_closed_pipe_ends_the_json_report_quietly() { + let directory = wide_tree(100, 50); + let (status, stderr) = + run_closed_pipe(directory.path(), &["check", "--format", "json", "."], 10); + assert!( + status.success(), + "expected a quiet exit, got {status:?} with stderr:\n{stderr}" + ); + assert_eq!(stderr, ""); +} + +/// The human report is written the same way, so it ends the same way. +#[test] +fn a_closed_pipe_ends_the_human_report_quietly() { + let directory = wide_tree(100, 50); + let (status, stderr) = run_closed_pipe(directory.path(), &["check", "."], 10); + assert!( + status.success(), + "expected a quiet exit, got {status:?} with stderr:\n{stderr}" + ); + assert_eq!(stderr, ""); +} + +/// So are the machine formats that serialize straight into standard output. +#[test] +fn a_closed_pipe_ends_the_sarif_report_quietly() { + let directory = wide_tree(100, 50); + let (status, stderr) = + run_closed_pipe(directory.path(), &["check", "--format", "sarif", "."], 10); + assert!( + status.success(), + "expected a quiet exit, got {status:?} with stderr:\n{stderr}" + ); + assert_eq!(stderr, ""); +} + +/// A short report can lose its reader before it writes its first byte. The +/// listing commands must survive that too. +#[test] +fn a_pipe_closed_before_the_first_byte_ends_languages_quietly() { + let directory = tempfile::tempdir().unwrap(); + let (status, stderr) = run_closed_pipe(directory.path(), &["languages"], 0); + assert!( + status.success(), + "expected a quiet exit, got {status:?} with stderr:\n{stderr}" + ); + assert_eq!(stderr, ""); +} + +/// `clap_complete` writes straight into the handle it is handed and panics if +/// that write fails, so the completion script is buffered before it is written. +#[test] +fn a_pipe_closed_before_the_first_byte_ends_completions_quietly() { + let directory = tempfile::tempdir().unwrap(); + let (status, stderr) = run_closed_pipe(directory.path(), &["completions", "zsh"], 0); + assert!( + status.success(), + "expected a quiet exit, got {status:?} with stderr:\n{stderr}" + ); + assert_eq!(stderr, ""); +} + +/// Run the binary with its standard error piped to a reader that closes at +/// once, and report how it ended. +fn run_closed_error_pipe(directory: &Path, arguments: &[&str]) -> ExitStatus { + let mut child = Command::new(binary()) + .current_dir(directory) + .env("PATH", "/usr/bin:/bin") + .args(arguments) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + drop(child.stderr.take().expect("standard error was piped")); + child.wait().unwrap() +} + +/// Standard error carries commentary, not the product of the run, so losing +/// its reader changes nothing: `-v` still reports its verdict through the exit +/// status instead of dying on the trace it could not write. +#[test] +fn a_closed_error_pipe_does_not_end_the_run() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + let status = run_closed_error_pipe(directory.path(), &["check", "-v", "."]); + assert_eq!( + status.code(), + Some(1), + "a closed standard error changed the verdict: {status:?}" + ); +} + +/// A closed pipe is benign only when it is *our* report that lost its reader. +/// `git hash-object` exiting before it reads the rewritten blob breaks a pipe +/// the run owns in the other direction: the index was never updated, so the +/// run must report the failure instead of ending quietly with success. +#[cfg(unix)] +#[test] +fn a_broken_pipe_from_git_hash_object_fails_the_staged_fix() { + use std::os::unix::fs::PermissionsExt; + + let directory = repository(); + // The blob has to outgrow any pipe buffer, so the write is still in flight + // when the fake `git hash-object` drops the reading end. + let mut source = String::new(); + for index in 0..8000 { + source.push_str(&format!("let value{index} = {index}; // remove {index}\n")); + } + let path = directory.path().join("wide.rs"); + fs::write(&path, &source).unwrap(); + git(directory.path(), &["add", "wide.rs"]); + let staged_before = git(directory.path(), &["show", ":wide.rs"]); + + // Every invocation reaches the real Git except `hash-object`, which closes + // its standard input and fails without reading a byte. + let fake = tempfile::tempdir().unwrap(); + let script = fake.path().join("git"); + fs::write( + &script, + "#!/bin/sh\n\ + if [ \"$1\" = hash-object ]; then\n\ + exec 0<&-\n\ + exit 1\n\ + fi\n\ + exec /usr/bin/git \"$@\"\n", + ) + .unwrap(); + fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap(); + + let output = Command::new(binary()) + .current_dir(directory.path()) + .env("PATH", format!("{}:/usr/bin:/bin", fake.path().display())) + .args(["fix", "--staged"]) + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(2), + "a failed blob write ended the run quietly; stderr was:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.is_empty(), + "the failed staged fix was never reported on standard error" + ); + assert!( + stderr.contains("git hash-object"), + "the report does not say which write failed:\n{stderr}" + ); + assert_eq!( + git(directory.path(), &["show", ":wide.rs"]), + staged_before, + "the index changed although no blob was written" + ); + assert_eq!( + fs::read(&path).unwrap(), + source.as_bytes(), + "the working tree changed although no blob was written" + ); +} + +/// `fix` refuses standard input, so its `--help` must not offer it as a target. +#[test] +fn fix_help_does_not_advertise_the_standard_input_it_refuses() { + let directory = tempfile::tempdir().unwrap(); + let fixed = run(directory.path(), &["fix", "--help"]); + assert_eq!(fixed.status.code(), Some(0)); + let help = String::from_utf8(fixed.stdout).unwrap(); + assert!( + !help.contains("reads standard input"), + "`fix --help` advertises a target it refuses:\n{help}" + ); + assert!( + help.contains("Files or directories to rewrite"), + "`fix --help` does not describe its PATH list:\n{help}" + ); + let checked = run(directory.path(), &["check", "--help"]); + assert_eq!(checked.status.code(), Some(0)); + let help = String::from_utf8(checked.stdout).unwrap(); + assert!( + help.contains("reads standard input"), + "`check --help` stopped documenting `-`:\n{help}" + ); +} + +/// The counter line is erased only if one was ever drawn: a run that scans +/// nothing must not write an escape sequence to a terminal that saw no counter. +#[test] +fn progress_clears_the_counter_only_when_one_was_drawn() { + let empty = tempfile::tempdir().unwrap(); + let output = run(empty.path(), &["check", "--progress", "always", "."]); + assert_eq!(output.status.code(), Some(0)); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!( + !stderr.contains("\r\x1b[2K"), + "a counter that was never drawn was cleared anyway:\n{stderr:?}" + ); + + let directory = many_files(120); + let output = run(directory.path(), &["check", "--progress", "always", "."]); + assert_eq!(output.status.code(), Some(1)); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!( + stderr.contains("\r\x1b[2K"), + "the counter line was never cleared:\n{stderr:?}" + ); +} + +/// "Nothing to check" is the vocabulary of `check`. Every command has its own +/// verb for the run that found nothing to work on. +#[test] +fn an_empty_run_summarizes_itself_in_the_vocabulary_of_its_command() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("notes.md"), b"# notes\n").unwrap(); + for (arguments, expected) in [ + (vec!["check", "notes.md"], "Nothing to check.\n"), + (vec!["fix", "notes.md"], "Nothing to fix.\n"), + (vec!["fix", "--dry-run", "notes.md"], "Nothing to fix.\n"), + (vec!["diff", "notes.md"], "Nothing to diff.\n"), + (vec!["scan", "notes.md"], "Nothing to scan.\n"), + ] { + let output = run(directory.path(), &arguments); + assert_eq!(output.status.code(), Some(0), "`ocomment {arguments:?}`"); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + expected, + "`ocomment {arguments:?}`" + ); + } + let output = run(directory.path(), &["scan", "."]); + assert_eq!(output.status.code(), Some(0)); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "Nothing to scan: 1 file skipped (unknown language: 1; use -v to list).\n" + ); +} From 807eb0e61d31ffb4f5f937c512b75e7cc292ea0d Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Wed, 26 Aug 2026 02:53:29 +0900 Subject: [PATCH 03/21] feat(cli): actionable error messages, init --force/--stdout, docs for the new CLI surface - every config/git/atomic/plugin/files error says what to do next (version = 1 hint, `ocomment languages`, --staged needs a Git repository, stale index.lock, rollback journal, missing curl/gh/oras/cosign name their purpose and point at `ocomment doctor`) - --policy all without --force-protected reports the kept preamble comments - init: refuse to overwrite without --force, --stdout prints the template, note when a parent .ocomment.toml already applies - fix --dry-run lists skipped paths like fix does - source_guards test pins that every stdout write goes through output::wrote; staged broken-pipe test runs in <1s and resolves git from PATH - README quick start, CHANGELOG, CONTRIBUTING output conventions, man page == `ocomment man` consistency test Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RLSWPMQ9PvtGi3jyBF7bJx --- CHANGELOG.md | 22 + CONTRIBUTING.md | 14 + README.md | 26 +- docs/ocomment.1 | 134 +++++ release-extras/_ocomment | 2 + release-extras/_ocomment.ps1 | 2 + release-extras/ocomment.1 | 134 +++++ release-extras/ocomment.bash | 2 +- release-extras/ocomment.elv | 2 + release-extras/ocomment.fish | 2 + rust/ocomment/src/atomic.rs | 44 +- rust/ocomment/src/cli.rs | 214 +++++++- rust/ocomment/src/config.rs | 12 +- rust/ocomment/src/files.rs | 30 +- rust/ocomment/src/git.rs | 10 +- rust/ocomment/src/output.rs | 56 ++- rust/ocomment/src/plugin.rs | 63 ++- rust/ocomment/tests/cli.rs | 708 ++++++++++++++++++++++++++- rust/ocomment/tests/source_guards.rs | 208 ++++++++ 19 files changed, 1609 insertions(+), 76 deletions(-) create mode 100644 rust/ocomment/tests/source_guards.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e487d1b..e031869 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,3 +14,25 @@ All notable changes to OComment will be documented here. The project follows - Independent OCaml reference implementation and byte-for-byte differential fixtures. - Cross-platform CI, packaging definitions, and release verification gates. +- Full `--help` for every command and every possible value, an exit-status, + files, and examples epilogue, and an `ocomment man` subcommand that renders + the manual page. +- `-q`/`--quiet`, `-v`/`--verbose`, a `--progress` live scanning counter, and a + one-line preview of the reported comment that `--no-preview` turns off. +- `-` as a target: `check`, `diff`, and `scan` read standard input under the + `` pseudo-path. +- `fix --dry-run`, which prints the patch `fix` would apply and writes nothing. +- `init --force` and `init --stdout`; `init` otherwise refuses to overwrite an + existing file and notes a configuration that already applies to the directory. + +### Changed + +- Human output names comment kinds in their canonical kebab-case spelling + (`doc-block`, `html-comment`) rather than leaking Rust `Debug` spellings. +- The manual page and the shell completions are generated from the binary, and + the checked-in copies are verified against it. +- Run summaries and notes go to standard error, leaving standard output to the + findings, patches, and machine formats alone. +- Failures say what to do next: how to add `version = 1`, which flag forces a + language, how to clear a stale `.git/index.lock`, and which missing tool + `ocomment doctor` diagnoses. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 757ddc5..0636de5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,6 +56,20 @@ case. Keep byte spans half-open, edits sorted and non-overlapping, and output deterministic. Update both implementations and their differential expectations when the shared contract changes. +## CLI output conventions + +Findings, patches, generated files, and every machine format are written to +standard output; run summaries, progress, and notes are written to standard +error, so `ocomment diff > fix.patch` and `--format json | jq` stay clean. A +machine format writes nothing to standard error but errors and diagnostics. +Every write to standard output goes through `output::wrote(...)`, which tags a +lost reader as `OutputPipeClosed` so the run ends quietly instead of reporting +an unexplained broken pipe; `rust/ocomment/tests/source_guards.rs` enforces +that. Name a language, dialect, comment kind, policy, layout, or disposition +through its `as_str()` and never through `Debug`: the canonical spellings are +kebab-case (`doc-block`, `html-comment`) and are shared with the human, JSON, +JSONL, SARIF, and GitHub output. All user-facing text is English. + ## Pull requests - Keep each pull request focused and explain compatibility or safety effects. diff --git a/README.md b/README.md index 4070184..ea3840f 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,34 @@ cargo install ocomment --locked ocomment # check the current repository ocomment check src tests ocomment diff src +ocomment fix --dry-run src ocomment fix src printf '%s\n' 'let x = 1; // remove' | ocomment strip --language rust ``` +A human run previews each removable comment and closes with a summary: + +```console +$ ocomment check src +src/main.rs:2:5: removable line comment: // TODO: drop this +Found 1 removable comment in 1 file (1 file scanned). Run `ocomment fix` to remove it. +``` + +Findings, patches, and machine formats go to standard output; the summary and +every note go to standard error, so `ocomment diff src > fix.patch` keeps the +patch clean. `-q` drops the summary and leaves `check` to answer with its exit +code, while `diff` and `scan` still write the patch or listing they exist for; +`-v` traces what was scanned and counts every comment kind; `--no-preview` +drops the previewed text. A `-` target reads standard input under the `` +pseudo-path, and `fix --dry-run` prints the patch `fix` would apply without +writing a file. + `check` exits 0 when clean, 1 when removable comments exist, and 2 for an -invalid source, configuration, plugin, or I/O failure. `diff` exits 1 when it -prints a change. Successful `fix` and `strip` operations exit 0. JSON, JSONL, -SARIF, and GitHub annotation output are available through `--format`. +invalid source, configuration, plugin, or I/O failure. `diff` and +`fix --dry-run` exit 1 when they print a change. Successful `fix` and `strip` +operations exit 0. JSON, JSONL, SARIF, and GitHub annotation output are +available through `--format`. Run `ocomment --help` for every option and +`ocomment man` for the manual page. The default `safe` policy removes ordinary and documentation comments while keeping source preambles and tool/language directives. `legal` additionally diff --git a/docs/ocomment.1 b/docs/ocomment.1 index 5dba5e4..11c6dea 100644 --- a/docs/ocomment.1 +++ b/docs/ocomment.1 @@ -316,6 +316,140 @@ Render the roff manual page to stdout .TP ocomment\-help(1) Print this message or the help of the given subcommand(s) +.SH COMMAND OPTIONS +.SS ocomment check +.TP +\fB\-\-staged\fR +Read and update Git index blobs rather than treating the working tree as the source +.TP +\fB\-\-index\-only\fR +With `\-\-staged`, do not attempt a uniquely mappable working\-tree update +.TP +[\fIPATH\fR] +Files or directories to process; `\-` reads standard input (default: current directory) +.SS ocomment fix +.TP +\fB\-\-staged\fR +Read and update Git index blobs rather than treating the working tree as the source +.TP +\fB\-\-index\-only\fR +With `\-\-staged`, do not attempt a uniquely mappable working\-tree update +.TP +\fB\-\-dry\-run\fR +Print the patch `fix` would apply and write nothing +.TP +[\fIPATH\fR] +Files or directories to rewrite (default: current directory) +.SS ocomment diff +.TP +\fB\-\-staged\fR +Read and update Git index blobs rather than treating the working tree as the source +.TP +\fB\-\-index\-only\fR +With `\-\-staged`, do not attempt a uniquely mappable working\-tree update +.TP +[\fIPATH\fR] +Files or directories to process; `\-` reads standard input (default: current directory) +.SS ocomment scan +.TP +\fB\-\-staged\fR +Read and update Git index blobs rather than treating the working tree as the source +.TP +\fB\-\-index\-only\fR +With `\-\-staged`, do not attempt a uniquely mappable working\-tree update +.TP +[\fIPATH\fR] +Files or directories to process; `\-` reads standard input (default: current directory) +.SS ocomment init +.TP +\fB\-\-fix\fR +For the Lefthook hook, run `fix` instead of `check` +.TP +\fB\-\-force\fR +Replace the file if it already exists +.TP +\fB\-\-stdout\fR +Print the template to standard output and write no file +.TP +[\fIKIND\fR] [default: config] +Which starter file to write +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +config +.IP \(bu 2 +lefthook +.RE +.SS ocomment config +.TP +[\fIACTION\fR] [default: show] +Which view of the resolved configuration to print +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +show +.IP \(bu 2 +locate +.IP \(bu 2 +explain +.IP \(bu 2 +schema +.RE +.SS ocomment plugin add +.TP +\fB\-\-name\fR \fI\fR +Name to register the plugin under (default: the file stem) +.TP +\fB\-\-sha256\fR \fI\fR +Expected SHA\-256 digest of the component, verified before install +.TP +\fB\-\-identity\fR \fI\fR +Publisher identity recorded alongside the pinned digest +.TP +<\fISOURCE\fR> +Path or URL of the WASM component to install +.SS ocomment plugin remove +.TP +<\fINAME\fR> +Name of the plugin to remove +.SS ocomment plugin update +.TP +[\fINAME\fR] +Name of the plugin to update (default: all of them) +.SS ocomment plugin verify +.TP +[\fINAME\fR] +Name of the plugin to verify (default: all of them) +.SS ocomment plugin new +.TP +<\fIPATH\fR> +Directory to create the plugin crate in +.SS ocomment completions +.TP +<\fISHELL\fR> +Shell whose completion script is written to stdout +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +bash +.IP \(bu 2 +elvish +.IP \(bu 2 +fish +.IP \(bu 2 +powershell +.IP \(bu 2 +zsh +.RE .SH VERSION v0.1.0 .SH EXIT STATUS diff --git a/release-extras/_ocomment b/release-extras/_ocomment index b878433..2e59b77 100644 --- a/release-extras/_ocomment +++ b/release-extras/_ocomment @@ -642,6 +642,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--hyperlinks=[When to emit terminal hyperlinks for reported paths]:WHEN:(auto always never)' \ '--progress=[When to draw the live scanning counter on standard error]:WHEN:(auto always never)' \ '--fix[For the Lefthook hook, run \`fix\` instead of \`check\`]' \ +'(--stdout)--force[Replace the file if it already exists]' \ +'--stdout[Print the template to standard output and write no file]' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ diff --git a/release-extras/_ocomment.ps1 b/release-extras/_ocomment.ps1 index 9393203..907a86d 100644 --- a/release-extras/_ocomment.ps1 +++ b/release-extras/_ocomment.ps1 @@ -219,6 +219,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--hyperlinks', '--hyperlinks', [CompletionResultType]::ParameterName, 'When to emit terminal hyperlinks for reported paths') [CompletionResult]::new('--progress', '--progress', [CompletionResultType]::ParameterName, 'When to draw the live scanning counter on standard error') [CompletionResult]::new('--fix', '--fix', [CompletionResultType]::ParameterName, 'For the Lefthook hook, run `fix` instead of `check`') + [CompletionResult]::new('--force', '--force', [CompletionResultType]::ParameterName, 'Replace the file if it already exists') + [CompletionResult]::new('--stdout', '--stdout', [CompletionResultType]::ParameterName, 'Print the template to standard output and write no file') [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') diff --git a/release-extras/ocomment.1 b/release-extras/ocomment.1 index 5dba5e4..11c6dea 100644 --- a/release-extras/ocomment.1 +++ b/release-extras/ocomment.1 @@ -316,6 +316,140 @@ Render the roff manual page to stdout .TP ocomment\-help(1) Print this message or the help of the given subcommand(s) +.SH COMMAND OPTIONS +.SS ocomment check +.TP +\fB\-\-staged\fR +Read and update Git index blobs rather than treating the working tree as the source +.TP +\fB\-\-index\-only\fR +With `\-\-staged`, do not attempt a uniquely mappable working\-tree update +.TP +[\fIPATH\fR] +Files or directories to process; `\-` reads standard input (default: current directory) +.SS ocomment fix +.TP +\fB\-\-staged\fR +Read and update Git index blobs rather than treating the working tree as the source +.TP +\fB\-\-index\-only\fR +With `\-\-staged`, do not attempt a uniquely mappable working\-tree update +.TP +\fB\-\-dry\-run\fR +Print the patch `fix` would apply and write nothing +.TP +[\fIPATH\fR] +Files or directories to rewrite (default: current directory) +.SS ocomment diff +.TP +\fB\-\-staged\fR +Read and update Git index blobs rather than treating the working tree as the source +.TP +\fB\-\-index\-only\fR +With `\-\-staged`, do not attempt a uniquely mappable working\-tree update +.TP +[\fIPATH\fR] +Files or directories to process; `\-` reads standard input (default: current directory) +.SS ocomment scan +.TP +\fB\-\-staged\fR +Read and update Git index blobs rather than treating the working tree as the source +.TP +\fB\-\-index\-only\fR +With `\-\-staged`, do not attempt a uniquely mappable working\-tree update +.TP +[\fIPATH\fR] +Files or directories to process; `\-` reads standard input (default: current directory) +.SS ocomment init +.TP +\fB\-\-fix\fR +For the Lefthook hook, run `fix` instead of `check` +.TP +\fB\-\-force\fR +Replace the file if it already exists +.TP +\fB\-\-stdout\fR +Print the template to standard output and write no file +.TP +[\fIKIND\fR] [default: config] +Which starter file to write +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +config +.IP \(bu 2 +lefthook +.RE +.SS ocomment config +.TP +[\fIACTION\fR] [default: show] +Which view of the resolved configuration to print +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +show +.IP \(bu 2 +locate +.IP \(bu 2 +explain +.IP \(bu 2 +schema +.RE +.SS ocomment plugin add +.TP +\fB\-\-name\fR \fI\fR +Name to register the plugin under (default: the file stem) +.TP +\fB\-\-sha256\fR \fI\fR +Expected SHA\-256 digest of the component, verified before install +.TP +\fB\-\-identity\fR \fI\fR +Publisher identity recorded alongside the pinned digest +.TP +<\fISOURCE\fR> +Path or URL of the WASM component to install +.SS ocomment plugin remove +.TP +<\fINAME\fR> +Name of the plugin to remove +.SS ocomment plugin update +.TP +[\fINAME\fR] +Name of the plugin to update (default: all of them) +.SS ocomment plugin verify +.TP +[\fINAME\fR] +Name of the plugin to verify (default: all of them) +.SS ocomment plugin new +.TP +<\fIPATH\fR> +Directory to create the plugin crate in +.SS ocomment completions +.TP +<\fISHELL\fR> +Shell whose completion script is written to stdout +.br + +.br +\fIPossible values:\fR +.RS 14 +.IP \(bu 2 +bash +.IP \(bu 2 +elvish +.IP \(bu 2 +fish +.IP \(bu 2 +powershell +.IP \(bu 2 +zsh +.RE .SH VERSION v0.1.0 .SH EXIT STATUS diff --git a/release-extras/ocomment.bash b/release-extras/ocomment.bash index 81a8f2e..dccf423 100644 --- a/release-extras/ocomment.bash +++ b/release-extras/ocomment.bash @@ -867,7 +867,7 @@ _ocomment() { return 0 ;; ocomment__subcmd__init) - opts="-q -v -h --fix --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help config lefthook" + opts="-q -v -h --fix --force --stdout --config --policy --layout --language --dialect --keep-kind --remove-kind --force-invalid --force-protected --format --color --hyperlinks --no-preview --progress --quiet --verbose --help config lefthook" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/release-extras/ocomment.elv b/release-extras/ocomment.elv index 715e11b..2c36aa9 100644 --- a/release-extras/ocomment.elv +++ b/release-extras/ocomment.elv @@ -209,6 +209,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --hyperlinks 'When to emit terminal hyperlinks for reported paths' cand --progress 'When to draw the live scanning counter on standard error' cand --fix 'For the Lefthook hook, run `fix` instead of `check`' + cand --force 'Replace the file if it already exists' + cand --stdout 'Print the template to standard output and write no file' cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' diff --git a/release-extras/ocomment.fish b/release-extras/ocomment.fish index 5b99baa..a29b8b9 100644 --- a/release-extras/ocomment.fish +++ b/release-extras/ocomment.fish @@ -683,6 +683,8 @@ complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l progress -d ' always\t'' never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l fix -d 'For the Lefthook hook, run `fix` instead of `check`' +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l force -d 'Replace the file if it already exists' +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l stdout -d 'Print the template to standard output and write no file' complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' diff --git a/rust/ocomment/src/atomic.rs b/rust/ocomment/src/atomic.rs index 6cda7a8..69c1116 100644 --- a/rust/ocomment/src/atomic.rs +++ b/rust/ocomment/src/atomic.rs @@ -54,7 +54,14 @@ pub fn apply_transaction(plans: Vec) -> Result<()> { std::process::id() )); if backup.exists() { - bail!("rollback path {} already exists", backup.display()); + // The journal holds the file as it was before the interrupted + // run, so deleting it unread can be the loss the rollback existed + // to prevent. + bail!( + "rollback path {} already exists; a previous ocomment run may have been \ + interrupted — inspect and delete it before retrying", + backup.display() + ); } prepared.push(Prepared { plan, @@ -166,4 +173,39 @@ mod tests { permissions.readonly() ); } + + /// A journal left over from an interrupted run is the only thing standing + /// between the caller and a retry, and it holds the pre-run contents of a + /// file. The refusal has to say both: what the file is, and that reading + /// it before deleting it is the point. + /// + /// The name carries this process's own id, so the test can plant exactly + /// the journal the transaction is about to reach for. + #[test] + fn an_existing_rollback_journal_says_what_to_do_about_it() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("x.rs"); + fs::write(&path, b"old").unwrap(); + let journal = directory + .path() + .join(format!(".x.rs.ocomment-rollback-{}-0", std::process::id())); + fs::write(&journal, b"interrupted").unwrap(); + + let error = apply_transaction(vec![WritePlan { + path: path.clone(), + original: b"old".to_vec(), + replacement: b"new".to_vec(), + }]) + .unwrap_err(); + + assert_eq!( + error.to_string(), + format!( + "rollback path {} already exists; a previous ocomment run may have been \ + interrupted — inspect and delete it before retrying", + journal.display() + ) + ); + assert_eq!(fs::read(&path).unwrap(), b"old", "the file was rewritten"); + } } diff --git a/rust/ocomment/src/cli.rs b/rust/ocomment/src/cli.rs index 7e0fe14..02bfcdd 100644 --- a/rust/ocomment/src/cli.rs +++ b/rust/ocomment/src/cli.rs @@ -347,6 +347,12 @@ struct InitArgs { /// For the Lefthook hook, run `fix` instead of `check`. #[arg(long)] fix: bool, + /// Replace the file if it already exists. + #[arg(long, conflicts_with = "stdout")] + force: bool, + /// Print the template to standard output and write no file. + #[arg(long)] + stdout: bool, } #[derive(Clone, Copy, Debug, Default, ValueEnum)] @@ -567,6 +573,7 @@ fn run_target( dry_run, force_invalid: resolved.config.policy.force_invalid, applied, + policy: resolved.config.policy.mode, }, )?; if invalid { @@ -725,26 +732,116 @@ fn apply_cli_overrides(config: &mut config::Config, common: &CommonArgs) { } } +/// The apostrophe definition `roff` writes at the top of every fragment it +/// renders. A page needs it once, so it is stripped from every fragment after +/// the first. +const ROFF_PREAMBLE: &str = concat!(r".ie \n(.g .ds Aq \(aq", "\n", r".el .ds Aq '", "\n"); + +/// Append one rendered `roff` fragment to the page under construction. +fn append_fragment(page: &mut String, fragment: &[u8]) -> Result<()> { + let text = std::str::from_utf8(fragment).context("the manual page is not valid UTF-8")?; + page.push_str(text.strip_prefix(ROFF_PREAMBLE).unwrap_or(text)); + Ok(()) +} + +/// Render the arguments that belong to one command alone, as `.SS` subsections. +/// +/// `clap_mangen` renders a single page for the root command, so an argument +/// declared on a subcommand — `fix --dry-run`, `init --force`, `plugin add +/// --sha256` — would never reach the manual at all. Every command is walked +/// and the arguments it does not inherit are written under its own heading. +fn command_options(command: &clap::Command, path: &str, page: &mut String) -> Result<()> { + for subcommand in command.get_subcommands() { + if subcommand.is_hide_set() || subcommand.get_name() == "help" { + continue; + } + let name = format!("{path} {}", subcommand.get_name()); + // The global arguments already have one entry each under OPTIONS, + // POLICY, and OUTPUT, and `--help` is on every command by definition. + // Repeating them here would bury the few arguments this section is + // for. Hiding is how `clap_mangen` is told to skip an argument. + let mut own = subcommand.clone(); + let inherited: Vec = own + .get_arguments() + .filter(|argument| { + argument.is_global_set() + || argument.get_id() == "help" + || argument.get_id() == "version" + }) + .map(|argument| argument.get_id().clone()) + .collect(); + for id in inherited { + own = own.mut_arg(id, |argument| argument.hide(true)); + } + let mut fragment = Vec::new(); + clap_mangen::Man::new(own) + .render_options_section(&mut fragment) + .context("cannot render the manual page")?; + let mut rendered = String::new(); + append_fragment(&mut rendered, &fragment)?; + // A command with nothing of its own renders an empty fragment, and an + // empty heading would claim otherwise. + if let Some(body) = rendered.strip_prefix(".SH OPTIONS\n") + && !body.is_empty() + { + page.push_str(&format!(".SS {}\n{body}", name.replace('-', "\\-"))); + } + command_options(subcommand, &name, page)?; + } + Ok(()) +} + /// Render the roff manual page from the parser definition itself. fn run_man() -> Result { - let mut page = Vec::new(); // `clap_mangen` renders `after_long_help` as one opaque `.SH EXTRA` body, // so the page is built without it and the same content is appended below - // as real roff sections. + // as real roff sections. It is assembled section by section rather than + // through `render`, because the per-command options belong next to the + // command list and `render` puts VERSION after it. // // The `.TH` date is left blank on purpose: stamping the build date would // make two reproducible builds of the same source disagree. - clap_mangen::Man::new(Cli::command().after_long_help(None)) + let man = clap_mangen::Man::new(Cli::command().after_long_help(None)) .title("OCOMMENT") - .manual("User Commands") - .render(&mut page) + .manual("User Commands"); + let mut page = String::new(); + let mut fragment = Vec::new(); + // The title fragment keeps the apostrophe definition the whole page needs. + man.render_title(&mut fragment) + .context("cannot render the manual page")?; + page.push_str(std::str::from_utf8(&fragment).context("the manual page is not valid UTF-8")?); + type Section = fn(&clap_mangen::Man, &mut dyn Write) -> io::Result<()>; + for section in [ + clap_mangen::Man::render_name_section as Section, + clap_mangen::Man::render_synopsis_section, + clap_mangen::Man::render_description_section, + clap_mangen::Man::render_options_section, + clap_mangen::Man::render_subcommands_section, + ] { + fragment.clear(); + section(&man, &mut fragment).context("cannot render the manual page")?; + append_fragment(&mut page, &fragment)?; + } + let mut per_command = String::new(); + let mut root = Cli::command(); + // Building propagates the global arguments into every subcommand, which is + // what makes them recognizable as inherited below. + root.build(); + command_options(&root, "ocomment", &mut per_command)?; + if !per_command.is_empty() { + page.push_str(".SH COMMAND OPTIONS\n"); + page.push_str(&per_command); + } + fragment.clear(); + man.render_version_section(&mut fragment) .context("cannot render the manual page")?; - if !page.ends_with(b"\n") { - page.push(b'\n'); + append_fragment(&mut page, &fragment)?; + if !page.ends_with('\n') { + page.push('\n'); } - page.extend_from_slice(MAN_SECTIONS.as_bytes()); + page.push_str(MAN_SECTIONS); let mut stdout = output::stdout(); - output::wrote(stdout.write_all(&page))?; + output::wrote(stdout.write_all(page.as_bytes()))?; output::finish(&mut stdout)?; Ok(0) } @@ -764,38 +861,101 @@ fn run_completions(shell: Shell) -> Result { } fn run_init(args: InitArgs) -> Result { - let mut stdout = output::stdout(); - match args.kind { - InitKind::Config => create_new( - &mut stdout, + // Writing the file is only the first half of the task, so each template + // carries the step that finishes it. + let (path, contents, next_step) = match args.kind { + InitKind::Config => ( config::CONFIG_FILE, - include_str!("../assets/default-config.toml"), - )?, + include_str!("../assets/default-config.toml").to_owned(), + "edit [policy] and run `ocomment check`", + ), InitKind::Lefthook => { let command = if args.fix { "ocomment fix --staged" } else { "ocomment check --staged" }; - create_new( - &mut stdout, + ( "lefthook.yml", - &format!("pre-commit:\n commands:\n ocomment:\n run: {command}\n"), - )?; + format!("pre-commit:\n commands:\n ocomment:\n run: {command}\n"), + "run `lefthook install` to activate the hook", + ) } + }; + let mut stdout = output::stdout(); + if args.stdout { + // Nothing is created, so nothing is said about creating it: the + // template alone is on standard output, ready to be redirected. + output::wrote(write!(stdout, "{contents}"))?; + output::finish(&mut stdout)?; + return Ok(0); } + note_inherited_config()?; + write_template(&mut stdout, path, &contents, args.force, next_step)?; output::finish(&mut stdout)?; Ok(0) } -fn create_new(output: &mut impl Write, path: &str, contents: &str) -> Result<()> { - let mut file = fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(path) - .with_context(|| format!("refusing to overwrite {path}"))?; - file.write_all(contents.as_bytes())?; - output::wrote(writeln!(output, "created {path}"))?; +/// Say so when a project configuration from a parent directory already governs +/// this directory. +/// +/// The starter file is about to layer over it rather than start from nothing, +/// and the hook a `lefthook` run installs will read it — either way the reader +/// is better off knowing before they start editing. It is a note and not a +/// refusal: a nested per-crate configuration is a normal thing to want. +/// +/// The search starts at the parent so that the file this very run is about to +/// write — or the one `--force` is replacing — is never reported as inherited. +fn note_inherited_config() -> Result<()> { + let Ok(directory) = std::env::current_dir() else { + return Ok(()); + }; + let Some(inherited) = directory.parent().and_then(config::locate_project) else { + return Ok(()); + }; + let stderr = io::stderr(); + let mut report = stderr.lock(); + output::note( + &mut report, + &format!( + "note: {} already applies to this directory", + inherited.display() + ), + ) +} + +/// Write one starter file, refusing an existing one unless `force` says +/// otherwise. +/// +/// The refusal is `create_new` rather than a prior `exists()` test: between +/// such a test and the open the file could appear, and never writing over +/// someone's edited configuration is the whole point of the check. +fn write_template( + output: &mut impl Write, + path: &str, + contents: &str, + force: bool, + next_step: &str, +) -> Result<()> { + let mut options = fs::OpenOptions::new(); + options.write(true); + if force { + options.create(true).truncate(true); + } else { + options.create_new(true); + } + let mut file = options.open(path).map_err(|error| { + if error.kind() == io::ErrorKind::AlreadyExists { + anyhow::anyhow!( + "{path} already exists; use --force to overwrite or --stdout to print the template" + ) + } else { + anyhow::Error::new(error).context(format!("cannot write {path}")) + } + })?; + file.write_all(contents.as_bytes()) + .with_context(|| format!("cannot write {path}"))?; + output::wrote(writeln!(output, "created {path} — {next_step}"))?; Ok(()) } diff --git a/rust/ocomment/src/config.rs b/rust/ocomment/src/config.rs index 114c37c..772c5d6 100644 --- a/rust/ocomment/src/config.rs +++ b/rust/ocomment/src/config.rs @@ -278,9 +278,9 @@ pub fn load(explicit: Option<&Path>) -> Result { fn validate_languages(config: &Config) -> Result<()> { for (name, language_config) in &config.languages { - let language: Language = name - .parse() - .map_err(|_| anyhow!("unknown language configuration key `{name}`"))?; + let language: Language = name.parse().map_err(|_| { + anyhow!("unknown language configuration key `{name}`; see `ocomment languages`") + })?; if let Some(dialect) = language_config.dialect { validate_dialect(language, dialect) .with_context(|| format!("invalid dialect for [languages.{name}]"))?; @@ -377,7 +377,11 @@ fn parse_layer(path: &Path, require_version: bool) -> Result { ) })?; if require_version && config.version != Some(1) { - bail!("{} must contain `version = 1`", path.display()); + // The path is repeated deliberately: the first half is the verdict on + // a file the reader may not have opened, the second is the edit that + // settles it, and an editor is opened on the second one. + let path = path.display(); + bail!("{path} must contain `version = 1` (add `version = 1` at the top of {path})"); } toml::from_str(&text).with_context(|| format!("cannot parse {}", path.display())) } diff --git a/rust/ocomment/src/files.rs b/rust/ocomment/src/files.rs index 8268562..1a9a303 100644 --- a/rust/ocomment/src/files.rs +++ b/rust/ocomment/src/files.rs @@ -4,7 +4,7 @@ use globset::{Glob, GlobSet, GlobSetBuilder}; use ignore::WalkBuilder; use ocomment_core::{DeclarativeProfile, Detection, Dialect, Language, detect_language}; use std::{ - fs, + env, fs, path::{Path, PathBuf}, }; @@ -46,6 +46,30 @@ pub const STDIN_PATH: &str = ""; pub const STDIN_LANGUAGE_HELP: &str = "cannot detect the language of standard input; \ pass --language (see `ocomment languages`)"; +/// Why a file OComment has no scanner for is passed over, and the two ways out +/// of it: consult the list of what is built in, or name a language anyway. +/// +/// The end-of-run summary must not repeat this sentence once per file, so it +/// folds the reason onto a short key of its own; `output::skip_label` is what +/// ties the two together. +pub const NO_LANGUAGE: &str = + "no built-in language for this file (see `ocomment languages`; use --language to force)"; + +/// Why a named path was not found. A relative path is resolved against the +/// working directory, which is exactly what a caller who typed it from the +/// wrong place cannot see, so the directory that was searched is named. +fn missing_path_reason() -> String { + env::current_dir().map_or_else( + |_| "path does not exist".to_owned(), + |cwd| { + format!( + "path does not exist (checked relative to {})", + cwd.display() + ) + }, + ) +} + /// Turn the bytes read from standard input into a source file the ordinary /// pipeline can process, or the skip that says why it cannot. Detection has no /// path to work with, so it is driven by `--language` or by the contents. @@ -198,7 +222,7 @@ fn discover_with_scope( } else { discovery.skipped.push(SkippedFile { path, - reason: "path does not exist".into(), + reason: missing_path_reason(), error: true, explicit: explicit_scope, }); @@ -331,7 +355,7 @@ fn load_one( if language == Language::Unknown && profile.is_none() && plugin.is_none() { discovery.skipped.push(SkippedFile { path: path.to_path_buf(), - reason: "unknown language".into(), + reason: NO_LANGUAGE.into(), error: false, explicit: explicit_path, }); diff --git a/rust/ocomment/src/git.rs b/rust/ocomment/src/git.rs index e848b2a..40f92bf 100644 --- a/rust/ocomment/src/git.rs +++ b/rust/ocomment/src/git.rs @@ -201,6 +201,7 @@ pub fn run_staged(request: StagedRequest<'_>) -> Result { dry_run, force_invalid: resolved.config.policy.force_invalid, applied, + policy: resolved.config.policy.mode, }, )?; if invalid { @@ -229,7 +230,10 @@ fn fix_index(root: &Path, entries: &[IndexEntry], index_only: bool) -> Result<() let original_index = fs::read(&index_path) .with_context(|| format!("cannot read Git index {}", index_path.display()))?; if index_path.with_file_name("index.lock").exists() { - bail!("Git index is locked; no files were modified"); + bail!( + "Git index is locked; no files were modified; another Git process may be \ + running, or remove a stale .git/index.lock" + ); } let mut temporary_index = NamedTempFile::new_in(index_path.parent().unwrap_or(root))?; temporary_index.write_all(&original_index)?; @@ -324,7 +328,9 @@ fn map_edits_uniquely(index: &[u8], working: &[u8], edits: &[Edit]) -> Result Result { let mut output = command_output( Command::new("git").args(["rev-parse", "--show-toplevel"]), - "not inside a Git repository", + // Git's own words follow: they name the directory it searched from, + // which is the difference between "wrong directory" and "no repository". + "--staged needs a Git repository", )?; trim_line_ending(&mut output); Ok(bytes_to_path(&output)) diff --git a/rust/ocomment/src/output.rs b/rust/ocomment/src/output.rs index df0511e..e442242 100644 --- a/rust/ocomment/src/output.rs +++ b/rust/ocomment/src/output.rs @@ -1,7 +1,7 @@ -use crate::files::SkippedFile; +use crate::files::{NO_LANGUAGE, SkippedFile}; use anyhow::Result; use clap::ValueEnum; -use ocomment_core::{ByteSpan, CommentKind, Language, TransformResult}; +use ocomment_core::{ByteSpan, CommentKind, Disposition, Language, Policy, TransformResult}; use serde::Serialize; use serde_json::{Value, json}; use similar::{ChangeTag, TextDiff}; @@ -68,6 +68,9 @@ pub struct RenderOptions { /// The run reached the disk. A `fix` blocked by invalid syntax or an I/O /// error leaves this false and must not claim any removal. pub applied: bool, + /// The policy the run was asked for. Only `all` promises to take every + /// comment out, so only `all` owes an explanation for the ones it keeps. + pub policy: Policy, } /// What one run found, counted once for the end-of-run summary. @@ -143,6 +146,9 @@ fn removable_count(file: &ProcessedFile) -> usize { } /// Fold a skip reason onto a short label the summary can group by. +/// +/// The per-file line says what to do about one file; the summary counts many, +/// so it trades the sentence for a key short enough to sit in a list of them. fn skip_label(reason: &str) -> &str { if reason.starts_with("larger than ") { "too large" @@ -150,11 +156,35 @@ fn skip_label(reason: &str) -> &str { "binary" } else if reason.starts_with("language disabled") { "language disabled" + } else if reason == NO_LANGUAGE { + "unknown language" } else { reason } } +/// The `Keep` reason the core scanner gives a shebang or encoding line that +/// `--force-protected` would have removed. It is one of the five reasons the +/// differential protocol freezes, so matching on it is stable; the end-to-end +/// test `policy_all_says_how_to_remove_a_kept_preamble` is what would catch it +/// drifting apart from the scanner. +const PROTECTED_PREAMBLE: &str = "required source preamble"; + +/// How many comments were kept only because `--force-protected` was absent. +/// +/// Counted from the disposition rather than from the comment kind: a shebang +/// held back by `--keep-kind shebang` stays kept whatever `--force-protected` +/// says, and advertising the flag for it would be a lie. +fn protected_preambles(files: &[ProcessedFile]) -> usize { + files + .iter() + .flat_map(|file| &file.result.report.comments) + .filter(|comment| { + matches!(&comment.disposition, Disposition::Keep { reason } if reason == PROTECTED_PREAMBLE) + }) + .count() +} + /// `1 file` / `2 files`: the count and its noun, pluralized by the regular /// rule. Every noun the summary counts goes through this. fn plural(count: usize, noun: &str) -> String { @@ -480,7 +510,12 @@ fn render_human( } } } - if operation != Operation::Diff { + // `diff` keeps standard output for the patch alone, so the skips it met + // are left to the summary on standard error. `fix --dry-run` is that same + // `diff` speaking for the `fix` it stands in for: a skipped path can be + // the whole answer to the run, and the preview owes the reader the reason + // exactly where `fix` would put it. + if operation != Operation::Diff || options.dry_run { for item in skipped { if !item.error && (quiet || !(item.explicit || verbose)) { continue; @@ -509,6 +544,21 @@ fn render_human( note(&mut report, &line)?; } note(&mut report, &summary_report(&summary, options, folded))?; + // Under any other policy a kept preamble is one of many deliberate keeps + // and saying so every run would be noise. `all` said it would take + // everything, so what it left behind is the surprise worth a line. + if options.policy == Policy::All { + let protected = protected_preambles(files); + if protected > 0 { + note( + &mut report, + &format!( + "{} kept; add --force-protected to remove them.", + comments(protected, "protected preamble") + ), + )?; + } + } if summary.invalid_files > 0 && !options.force_invalid { let (verb, pronoun) = if summary.invalid_files == 1 { ("has", "it") diff --git a/rust/ocomment/src/plugin.rs b/rust/ocomment/src/plugin.rs index 492b409..c8df5ad 100644 --- a/rust/ocomment/src/plugin.rs +++ b/rust/ocomment/src/plugin.rs @@ -814,6 +814,24 @@ self-contained and return sorted, non-overlapping, non-empty byte spans. Ok(()) } +/// What each external tool is needed for, in the words the command line uses. +/// One constant per purpose keeps the four spawn sites and `doctor` naming the +/// same thing. +const HTTPS_SOURCES: &str = "https:// plugin sources"; +const GH_SOURCES: &str = "gh: plugin sources"; +const OCI_SOURCES: &str = "oci: plugin sources"; +const SIGNATURE_VERIFICATION: &str = "--identity verification"; + +/// Why a tool OComment shells out to could not be started. +/// +/// The operating system says only "No such file or directory", which names +/// neither the missing binary nor the part of the run that wanted it. This +/// says both, and sends the reader to the command that reports every tool at +/// once instead of making them rediscover the next gap one failure at a time. +fn missing_tool(tool: &str, purpose: &str) -> String { + format!("cannot run `{tool}` (needed for {purpose}); run `ocomment doctor`") +} + fn fetch_remote(source: &str, directory: &Path) -> Result { let temporary = TemporaryPath::new(directory, ".wasm")?; if source.starts_with("https://") { @@ -828,7 +846,7 @@ fn fetch_remote(source: &str, directory: &Path) -> Result { .arg(temporary.path()) .arg(source) .status() - .context("cannot launch HTTPS plugin retrieval")?; + .with_context(|| missing_tool("curl", HTTPS_SOURCES))?; ensure!(status.success(), "plugin retrieval failed with {status}"); } else if let Some(spec) = source.strip_prefix("gh:") { let (repository, tag, asset) = parse_github_source(spec)?; @@ -845,7 +863,7 @@ fn fetch_remote(source: &str, directory: &Path) -> Result { ]) .arg(temporary.path()) .status() - .context("cannot launch GitHub plugin retrieval")?; + .with_context(|| missing_tool("gh", GH_SOURCES))?; ensure!(status.success(), "plugin retrieval failed with {status}"); } else { let specification = source.strip_prefix("oci:").expect("remote kind checked"); @@ -860,7 +878,7 @@ fn fetch_remote(source: &str, directory: &Path) -> Result { .args(["pull", reference, "--output"]) .arg(pulled.path()) .status() - .context("cannot launch OCI plugin retrieval")?; + .with_context(|| missing_tool("oras", OCI_SOURCES))?; ensure!(status.success(), "plugin retrieval failed with {status}"); let artifact = if let Some(relative) = artifact_path { let relative = Path::new(relative); @@ -913,7 +931,7 @@ fn verify_sigstore(source: &str, artifact: &Path, identity: &str, directory: &Pa ]) .arg(reference) .status() - .context("cannot launch cosign")?; + .with_context(|| missing_tool("cosign", SIGNATURE_VERIFICATION))?; ensure!(status.success(), "Sigstore verification failed"); return Ok(()); } @@ -933,7 +951,7 @@ fn verify_sigstore(source: &str, artifact: &Path, identity: &str, directory: &Pa ]) .arg(bundle.path()) .status() - .context("cannot retrieve GitHub Sigstore bundle")? + .with_context(|| missing_tool("gh", GH_SOURCES))? } else { let bundle_url = format!("{source}.sigstore.json"); Command::new("curl") @@ -947,7 +965,7 @@ fn verify_sigstore(source: &str, artifact: &Path, identity: &str, directory: &Pa .arg(bundle.path()) .arg(&bundle_url) .status() - .with_context(|| format!("cannot retrieve Sigstore bundle {bundle_url}"))? + .with_context(|| missing_tool("curl", HTTPS_SOURCES))? }; ensure!(download.success(), "cannot retrieve Sigstore bundle"); let status = Command::new("cosign") @@ -961,7 +979,7 @@ fn verify_sigstore(source: &str, artifact: &Path, identity: &str, directory: &Pa ]) .arg(artifact) .status() - .context("cannot launch cosign")?; + .with_context(|| missing_tool("cosign", SIGNATURE_VERIFICATION))?; if !status.success() { bail!("Sigstore verification failed"); } @@ -1258,6 +1276,37 @@ mod tests { assert!(result.report.comments.is_empty()); } + /// A tool that is not installed is the most common way a plugin command + /// fails, and the shell's "No such file or directory" names neither the + /// binary nor the reason this run wanted it. Every spawn site says both, + /// and points at the one command that reports the whole environment. + /// + /// `cosign` runs only after an artifact has already been fetched and + /// validated, which no offline test can arrange, so its wording is pinned + /// here rather than through the command line. + #[test] + fn a_missing_tool_names_itself_its_purpose_and_doctor() { + for (tool, purpose) in [ + ("curl", HTTPS_SOURCES), + ("gh", GH_SOURCES), + ("oras", OCI_SOURCES), + ("cosign", SIGNATURE_VERIFICATION), + ] { + assert_eq!( + missing_tool(tool, purpose), + format!("cannot run `{tool}` (needed for {purpose}); run `ocomment doctor`") + ); + } + assert_eq!( + missing_tool("gh", GH_SOURCES), + "cannot run `gh` (needed for gh: plugin sources); run `ocomment doctor`" + ); + assert_eq!( + missing_tool("cosign", SIGNATURE_VERIFICATION), + "cannot run `cosign` (needed for --identity verification); run `ocomment doctor`" + ); + } + #[test] fn rejects_core_modules_and_component_imports() { let module = wat::parse_str("(module)").unwrap(); diff --git a/rust/ocomment/tests/cli.rs b/rust/ocomment/tests/cli.rs index 7b20dfd..11991ad 100644 --- a/rust/ocomment/tests/cli.rs +++ b/rust/ocomment/tests/cli.rs @@ -1,4 +1,5 @@ use std::{ + collections::BTreeSet, fs, io::{Read, Write}, path::Path, @@ -71,6 +72,12 @@ fn git_with_path(directory: &Path, arguments: &[&str], path: &std::ffi::OsStr) - output.stdout } +/// What a file OComment has no built-in scanner for is skipped with. The +/// sentence is pinned literally by `an_unknown_language_skip_says_how_to_force_one`; +/// every other test names it through this constant. +const NO_LANGUAGE: &str = + "no built-in language for this file (see `ocomment languages`; use --language to force)"; + fn repository() -> TempDir { let directory = tempfile::tempdir().unwrap(); git(directory.path(), &["init", "-q"]); @@ -214,6 +221,190 @@ fn init_lefthook_preserves_partial_stage_contract() { assert!(!generated.contains("stage_fixed")); } +/// The starter file is a decision the reader may already have made +/// differently: a second `init` must not quietly replace the config they have +/// been editing, and the refusal has to name both ways out. +#[test] +fn init_config_refuses_to_overwrite_an_existing_file() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join(".ocomment.toml"); + let mine = b"version = 1\n[policy]\nmode = \"all\"\n"; + fs::write(&path, mine).unwrap(); + + let output = run(directory.path(), &["init", "config"]); + assert_eq!( + output.status.code(), + Some(2), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let error = String::from_utf8_lossy(&output.stderr); + assert!( + error.contains( + ".ocomment.toml already exists; use --force to overwrite or --stdout to print the \ + template" + ), + "{error}" + ); + assert_eq!( + fs::read(&path).unwrap(), + mine, + "the refusal edited the file" + ); +} + +/// The same refusal guards the hook file, and `--force` is the way past it. +#[test] +fn init_force_overwrites_an_existing_file() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join(".ocomment.toml"); + fs::write(&path, b"stale\n").unwrap(); + + let output = run(directory.path(), &["init", "config", "--force"]); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let written = fs::read_to_string(&path).unwrap(); + assert!(written.contains("version = 1"), "{written}"); + assert!( + !written.contains("stale"), + "the old bytes survived: {written}" + ); + + let hook = directory.path().join("lefthook.yml"); + fs::write(&hook, b"stale\n").unwrap(); + let refused = run(directory.path(), &["init", "lefthook"]); + assert_eq!(refused.status.code(), Some(2)); + assert!( + String::from_utf8_lossy(&refused.stderr).contains("lefthook.yml already exists"), + "{}", + String::from_utf8_lossy(&refused.stderr) + ); + assert_eq!(fs::read(&hook).unwrap(), b"stale\n"); + let forced = run(directory.path(), &["init", "lefthook", "--force"]); + assert_eq!(forced.status.code(), Some(0)); + assert!(fs::read_to_string(&hook).unwrap().contains("pre-commit:")); +} + +/// `--stdout` is the read-only door: the template goes to the pipe and the +/// working directory is left exactly as it was found. +#[test] +fn init_stdout_prints_the_template_and_writes_nothing() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["init", "config", "--stdout"]); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let printed = String::from_utf8(output.stdout).unwrap(); + assert!(printed.contains("version = 1"), "{printed}"); + assert!( + !printed.contains("created "), + "nothing was created, so nothing may claim it was: {printed}" + ); + assert!(!directory.path().join(".ocomment.toml").exists()); + + let hook = run(directory.path(), &["init", "lefthook", "--fix", "--stdout"]); + assert_eq!(hook.status.code(), Some(0)); + let printed = String::from_utf8(hook.stdout).unwrap(); + assert!(printed.contains("ocomment fix --staged"), "{printed}"); + assert!(!directory.path().join("lefthook.yml").exists()); +} + +/// A config in a parent directory already governs this one, so a new starter +/// file here layers over it rather than starting from nothing. The note says +/// so before the file is written, and does not stop it being written. +#[test] +fn init_notes_a_project_config_that_already_applies() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join(".ocomment.toml"), + b"version = 1\n[policy]\nmode = \"all\"\n", + ) + .unwrap(); + let nested = directory.path().join("crate"); + fs::create_dir(&nested).unwrap(); + + let output = run(&nested, &["init", "config"]); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let note = String::from_utf8_lossy(&output.stderr); + assert!( + note.contains("note: ") + && note.contains(".ocomment.toml already applies to this directory"), + "{note}" + ); + assert!( + nested.join(".ocomment.toml").is_file(), + "the note replaced the file" + ); + + // The config the run itself just created is this directory's own, not an + // inherited one, so a first `init` in a bare directory says nothing. + let bare = tempfile::tempdir().unwrap(); + let quiet = run(bare.path(), &["init", "config"]); + assert_eq!(quiet.status.code(), Some(0)); + assert!( + !String::from_utf8_lossy(&quiet.stderr).contains("already applies"), + "{}", + String::from_utf8_lossy(&quiet.stderr) + ); +} + +/// Creating the file is not the end of the task, so the line that reports it +/// names the step that is. +#[test] +fn init_success_messages_name_the_next_step() { + let directory = tempfile::tempdir().unwrap(); + let config = run(directory.path(), &["init", "config"]); + assert_eq!(config.status.code(), Some(0)); + assert_eq!( + String::from_utf8_lossy(&config.stdout).trim_end(), + "created .ocomment.toml \u{2014} edit [policy] and run `ocomment check`" + ); + + let hook = run(directory.path(), &["init", "lefthook"]); + assert_eq!(hook.status.code(), Some(0)); + assert_eq!( + String::from_utf8_lossy(&hook.stdout).trim_end(), + "created lefthook.yml \u{2014} run `lefthook install` to activate the hook" + ); +} + +/// One writes a file and the other refuses to; asking for both is a mistake +/// clap can catch before anything is opened. +#[test] +fn init_refuses_force_together_with_stdout() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["init", "config", "--force", "--stdout"]); + assert_eq!(output.status.code(), Some(2)); + let error = String::from_utf8_lossy(&output.stderr); + assert!(error.contains("--force"), "{error}"); + assert!(error.contains("--stdout"), "{error}"); + assert!(!directory.path().join(".ocomment.toml").exists()); +} + +/// The two new switches are documented where a reader looks for them. +#[test] +fn init_help_documents_force_and_stdout() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["init", "--help"]); + assert_eq!(output.status.code(), Some(0)); + let help = String::from_utf8(output.stdout).unwrap(); + for needle in ["--force", "--stdout"] { + assert!(help.contains(needle), "`{needle}` is missing from:\n{help}"); + } +} + #[test] fn staged_fix_does_not_stage_unrelated_working_tree_changes() { let directory = repository(); @@ -1139,7 +1330,7 @@ fn verbose_lists_the_folded_skips() { assert_eq!(output.status.code(), Some(1)); let stdout = String::from_utf8(output.stdout).unwrap(); assert!( - stdout.contains("notes.md: skipped: unknown language"), + stdout.contains(&format!("notes.md: skipped: {NO_LANGUAGE}")), "verbose check output is:\n{stdout}" ); } @@ -1152,7 +1343,7 @@ fn an_explicit_unknown_language_argument_is_still_listed() { assert_eq!(output.status.code(), Some(0)); let stdout = String::from_utf8(output.stdout).unwrap(); assert!( - stdout.contains("notes.md: skipped: unknown language"), + stdout.contains(&format!("notes.md: skipped: {NO_LANGUAGE}")), "check output is:\n{stdout}" ); } @@ -1244,9 +1435,14 @@ fn help_lists_the_verbosity_and_progress_flags() { "`check --help` lacks {needle}:\n{help}" ); } + assert!( + help.contains("live scanning counter"), + "`--progress` does not say that it draws the live counter:\n{help}" + ); assert!( !help.contains("progress indicator"), - "`--progress` still promises an indicator that is never drawn:\n{help}" + "`--progress` describes the live counter it draws, not a vague \ + indicator:\n{help}" ); } @@ -1489,7 +1685,7 @@ fn a_named_skip_is_not_counted_twice_in_the_summary() { let stdout = String::from_utf8(output.stdout).unwrap(); let stderr = String::from_utf8(output.stderr).unwrap(); assert!( - stdout.contains("notes.md: skipped: unknown language"), + stdout.contains(&format!("notes.md: skipped: {NO_LANGUAGE}")), "check output is:\n{stdout}" ); assert_eq!( @@ -1605,7 +1801,7 @@ fn a_run_of_only_named_skips_does_not_repeat_them() { assert!( String::from_utf8(output.stdout) .unwrap() - .contains("notes.md: skipped: unknown language") + .contains(&format!("notes.md: skipped: {NO_LANGUAGE}")) ); assert_eq!( String::from_utf8(output.stderr).unwrap(), @@ -1682,19 +1878,28 @@ fn undetectable_standard_input_asks_for_a_language() { ); } -/// `strip` and `check -` read the same standard input, so they must fail with -/// the same words when they cannot tell what it is. +/// `strip` and every command that accepts `-` read the same standard input, +/// so they must fail with the same words when they cannot tell what it is. +/// One constant is what makes that true; this test is what keeps it true. #[test] fn strip_and_check_agree_on_the_undetectable_input_message() { let directory = tempfile::tempdir().unwrap(); - let stripped = run_stdin(directory.path(), &["strip"], b"let x = 1; // note\n"); - let checked = run_stdin(directory.path(), &["check", "-"], b"let x = 1; // note\n"); - assert_eq!(stripped.status.code(), Some(2)); - assert_eq!(checked.status.code(), Some(2)); - assert_eq!( - String::from_utf8(stripped.stderr).unwrap(), - String::from_utf8(checked.stderr).unwrap() - ); + let expected = "ocomment: cannot detect the language of standard input; \ + pass --language (see `ocomment languages`)\n"; + for arguments in [ + vec!["strip"], + vec!["check", "-"], + vec!["diff", "-"], + vec!["scan", "-"], + ] { + let output = run_stdin(directory.path(), &arguments, b"let x = 1; // note\n"); + assert_eq!(output.status.code(), Some(2), "`ocomment {arguments:?}`"); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + expected, + "`ocomment {arguments:?}`" + ); + } } /// A pipe cannot be rewritten in place; `fix` says so and names the command @@ -1789,6 +1994,43 @@ fn fix_dry_run_on_a_clean_file_reports_nothing_to_fix() { ); } +/// A skipped path is the entire answer to the run, so the preview has to name +/// it: `fix --dry-run` lists the skip on standard output exactly as the `fix` +/// it stands in for does, instead of leaving a bare "Nothing to fix." with no +/// reason attached. Plain `diff` keeps its standard output for the patch. +#[test] +fn fix_dry_run_lists_a_skipped_path_the_way_fix_does() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("notes.md"), b"# notes\n").unwrap(); + + let previewed = run(directory.path(), &["fix", "--dry-run", "notes.md"]); + assert_eq!(previewed.status.code(), Some(0)); + assert_eq!( + String::from_utf8(previewed.stdout.clone()).unwrap(), + format!("notes.md: skipped: {NO_LANGUAGE}\n"), + "the preview never said why it had nothing to fix" + ); + assert_eq!( + String::from_utf8(previewed.stderr).unwrap(), + "Nothing to fix.\n" + ); + + let fixed = run(directory.path(), &["fix", "notes.md"]); + assert_eq!( + String::from_utf8(previewed.stdout).unwrap(), + String::from_utf8(fixed.stdout).unwrap(), + "the preview and the run it stands in for disagree about the skip" + ); + + let diffed = run(directory.path(), &["diff", "notes.md"]); + assert_eq!(diffed.status.code(), Some(0)); + assert_eq!( + String::from_utf8(diffed.stdout).unwrap(), + "", + "`diff` reserves its standard output for the patch" + ); +} + /// Both new entry points are discoverable from `--help`. #[test] fn help_documents_standard_input_and_the_dry_run() { @@ -2017,6 +2259,23 @@ fn a_closed_error_pipe_does_not_end_the_run() { ); } +/// The real `git` on this machine, found on `PATH` the way a shell finds it. +/// +/// A fake `git` planted ahead of it has to hand every other subcommand to the +/// genuine one by absolute path: the fake is first on `PATH` itself, so `exec +/// git` would only call it back. `/usr/bin/git` is the fallback for a `PATH` +/// that names none. +#[cfg(unix)] +fn real_git() -> std::path::PathBuf { + std::env::var_os("PATH") + .and_then(|path| { + std::env::split_paths(&path) + .map(|directory| directory.join("git")) + .find(|candidate| candidate.is_file()) + }) + .unwrap_or_else(|| std::path::PathBuf::from("/usr/bin/git")) +} + /// A closed pipe is benign only when it is *our* report that lost its reader. /// `git hash-object` exiting before it reads the rewritten blob breaks a pipe /// the run owns in the other direction: the index was never updated, so the @@ -2027,11 +2286,23 @@ fn a_broken_pipe_from_git_hash_object_fails_the_staged_fix() { use std::os::unix::fs::PermissionsExt; let directory = repository(); - // The blob has to outgrow any pipe buffer, so the write is still in flight - // when the fake `git hash-object` drops the reading end. + // What travels down the pipe is the blob with the comments already taken + // out, so it is that which has to outgrow the pipe buffer — 64 KiB on + // Linux — for the write to still be in flight when the fake + // `git hash-object` drops the reading end. Half again as much is margin + // enough. The file is therefore sized by the bytes that survive the fix + // rather than by its own length, and it carries them on a few long lines + // instead of many short ones: the run costs time per comment, and this + // test needs bytes. + let padding = "x".repeat(200); let mut source = String::new(); - for index in 0..8000 { - source.push_str(&format!("let value{index} = {index}; // remove {index}\n")); + let mut stripped = 0; + let mut index = 0; + while stripped < 96 * 1024 { + let code = format!("let value{index} = \"{padding}\";"); + stripped += code.len() + 1; + source.push_str(&format!("{code} // remove {index}\n")); + index += 1; } let path = directory.path().join("wide.rs"); fs::write(&path, &source).unwrap(); @@ -2044,12 +2315,15 @@ fn a_broken_pipe_from_git_hash_object_fails_the_staged_fix() { let script = fake.path().join("git"); fs::write( &script, - "#!/bin/sh\n\ - if [ \"$1\" = hash-object ]; then\n\ - exec 0<&-\n\ - exit 1\n\ - fi\n\ - exec /usr/bin/git \"$@\"\n", + format!( + "#!/bin/sh\n\ + if [ \"$1\" = hash-object ]; then\n\ + exec 0<&-\n\ + exit 1\n\ + fi\n\ + exec {} \"$@\"\n", + real_git().display() + ), ) .unwrap(); fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap(); @@ -2075,6 +2349,14 @@ fn a_broken_pipe_from_git_hash_object_fails_the_staged_fix() { stderr.contains("git hash-object"), "the report does not say which write failed:\n{stderr}" ); + // `hash-object` also exits non-zero, and that failure carries the same + // name. This is the test for the broken pipe, so the blob must have been + // in flight when the reader went away, not sitting whole in the buffer. + assert!( + stderr.contains("cannot write the rewritten blob"), + "the run failed before the blob was ever written, so the broken pipe \ + went untested:\n{stderr}" + ); assert_eq!( git(directory.path(), &["show", ":wide.rs"]), staged_before, @@ -2162,3 +2444,379 @@ fn an_empty_run_summarizes_itself_in_the_vocabulary_of_its_command() { "Nothing to scan: 1 file skipped (unknown language: 1; use -v to list).\n" ); } + +/// A file OComment has no scanner for is not "unknown": the skip line names +/// the list to consult and the flag that forces a language anyway. The folded +/// summary clause keeps the short key, so a walk over a hundred unreadable +/// extensions still reads as one clause instead of a hundred sentences. +#[test] +fn an_unknown_language_skip_says_how_to_force_one() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + fs::write(directory.path().join("notes.md"), b"# notes\n").unwrap(); + let output = run(directory.path(), &["check", "-v", "."]); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!( + stdout.contains( + "notes.md: skipped: no built-in language for this file \ + (see `ocomment languages`; use --language to force)" + ), + "the skip line never said what to do about it:\n{stdout}" + ); + assert!( + stderr.contains("1 file skipped (unknown language: 1)."), + "the folded clause stopped using the short key:\n{stderr}" + ); +} + +/// A path that was named and is not there says where it was looked for, so a +/// typo, a wrong working directory, and a deleted file are told apart without +/// a second run. +#[test] +fn a_missing_path_says_where_it_was_looked_for() { + let directory = tempfile::tempdir().unwrap(); + let cwd = fs::canonicalize(directory.path()).unwrap(); + let output = run(&cwd, &["check", "missing.rs"]); + assert_eq!(output.status.code(), Some(2)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains(&format!( + "missing.rs: error: path does not exist (checked relative to {})", + cwd.display() + )), + "check output is:\n{stdout}" + ); +} + +/// A project configuration without the version key is refused; saying which +/// line to add, and to which file, is the whole fix. +#[test] +fn a_configuration_without_a_version_says_how_to_add_one() { + let directory = tempfile::tempdir().unwrap(); + let config = directory.path().join(".ocomment.toml"); + fs::write(&config, b"[policy]\nmode = \"all\"\n").unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["check", "sample.rs"]); + assert_eq!(output.status.code(), Some(2)); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!( + stderr.contains(&format!( + "must contain `version = 1` (add `version = 1` at the top of {})", + config.display() + )), + "the version error never said what to write:\n{stderr}" + ); +} + +/// A misspelled `[languages.*]` key is refused by name; the fix is the list of +/// the languages that do exist. +#[test] +fn an_unknown_language_key_points_at_the_language_list() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join(".ocomment.toml"), + b"version = 1\n[languages.klingon]\n", + ) + .unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["check", "sample.rs"]); + assert_eq!(output.status.code(), Some(2)); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "ocomment: unknown language configuration key `klingon`; see `ocomment languages`\n" + ); +} + +/// `--staged` reads the index, so outside a repository the flag is the thing +/// to drop. Git's own words are kept: they say which directory was searched. +#[test] +fn staged_outside_a_repository_names_the_flag_and_quotes_git() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["check", "--staged"]); + assert_eq!(output.status.code(), Some(2)); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!( + stderr.contains("--staged needs a Git repository:"), + "the failure never named the flag that needed one:\n{stderr}" + ); + assert!( + stderr.contains("not a git repository"), + "Git's own explanation was dropped:\n{stderr}" + ); +} + +/// A lock file left behind by a crashed Git is indistinguishable from a Git +/// that is running right now, so the message offers both readings and the +/// path to delete. +#[test] +fn a_locked_git_index_says_what_to_do_about_the_lock() { + let directory = repository(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + git(directory.path(), &["add", "sample.rs"]); + fs::write(directory.path().join(".git/index.lock"), b"").unwrap(); + let output = run(directory.path(), &["fix", "--staged"]); + assert_eq!(output.status.code(), Some(2)); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!( + stderr.contains( + "Git index is locked; no files were modified; another Git process may be \ + running, or remove a stale .git/index.lock" + ), + "the lock failure never said what to do about it:\n{stderr}" + ); +} + +/// The plugin commands shell out to four tools. A missing one must name the +/// binary, say what this run wanted it for, and point at the command that +/// reports the whole environment at once. +#[test] +fn a_missing_plugin_tool_names_it_its_purpose_and_doctor() { + let directory = tempfile::tempdir().unwrap(); + // Pin the project root: without a configuration the walk upwards can find + // a repository marker above the temporary directory and install there. + fs::write(directory.path().join(".ocomment.toml"), b"version = 1\n").unwrap(); + let empty = tempfile::tempdir().unwrap(); + for (source, expected) in [ + ( + "https://example.invalid/scanner.wasm", + "cannot run `curl` (needed for https:// plugin sources); run `ocomment doctor`", + ), + ( + "gh:owner/repo@v1#scanner.wasm", + "cannot run `gh` (needed for gh: plugin sources); run `ocomment doctor`", + ), + ( + "oci:example.invalid/scanner:1", + "cannot run `oras` (needed for oci: plugin sources); run `ocomment doctor`", + ), + ] { + let output = Command::new(binary()) + .current_dir(directory.path()) + .env("PATH", empty.path()) + .args([ + "plugin", + "add", + source, + "--sha256", + "0000000000000000000000000000000000000000000000000000000000000000", + "--identity", + "publisher@example.test", + ]) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(2), "`plugin add {source}`"); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!( + stderr.contains(expected), + "`plugin add {source}` said:\n{stderr}" + ); + } +} + +/// `--policy all` means "take everything out", so the one thing it deliberately +/// leaves behind has to explain itself: the summary counts the kept preambles +/// and names the flag that removes them too. +#[test] +fn policy_all_says_how_to_remove_a_kept_preamble() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("a.py"), + b"#!/usr/bin/env python3\n# note\nx = 1\n", + ) + .unwrap(); + let hint = "1 protected preamble comment kept; add --force-protected to remove them."; + + let all = run(directory.path(), &["check", "--policy", "all", "a.py"]); + assert_eq!(all.status.code(), Some(1)); + let stderr = String::from_utf8(all.stderr).unwrap(); + assert!( + stderr.contains(hint), + "`--policy all` never explained the comment it kept:\n{stderr}" + ); + + // Nothing is protected any more, so there is nothing to explain. + let forced = run( + directory.path(), + &["check", "--policy", "all", "--force-protected", "a.py"], + ); + let stderr = String::from_utf8(forced.stderr).unwrap(); + assert!( + !stderr.contains("--force-protected"), + "the hint outlived the flag that answers it:\n{stderr}" + ); + + // Under `safe` the preamble is one of many deliberate keeps; singling it + // out would be noise on every run. + let safe = run(directory.path(), &["check", "a.py"]); + let stderr = String::from_utf8(safe.stderr).unwrap(); + assert!( + !stderr.contains("--force-protected"), + "a policy that keeps much more than preambles advertised the flag:\n{stderr}" + ); + + // A file with no preamble at all never mentions it. + fs::write(directory.path().join("b.py"), b"# note\nx = 1\n").unwrap(); + let plain = run(directory.path(), &["check", "--policy", "all", "b.py"]); + let stderr = String::from_utf8(plain.stderr).unwrap(); + assert!( + !stderr.contains("--force-protected"), + "a run that kept no preamble advertised the flag anyway:\n{stderr}" + ); +} + +/// A file that ships with the repository, resolved from the crate directory so +/// a test can read it from whatever temporary directory it runs in. +fn shipped(relative: &str) -> std::path::PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(relative) +} + +/// How `clap_mangen` writes one option name: every `-` escaped, the name in +/// bold. Help text that merely mentions a flag is rendered in roman, so this +/// matches a real entry rather than a passing reference in someone else's +/// description. +fn roff_option(flag: &str) -> String { + format!("\\fB{}\\fR", flag.replace('-', "\\-")) +} + +/// Every long flag the CLI shows a user, gathered by walking `--help` down +/// every subcommand. Descriptions are scanned too: a flag a description names +/// is a flag the reader will look up. +fn long_flags_in_help(directory: &Path, path: &[&str], found: &mut BTreeSet) { + let mut arguments = path.to_vec(); + arguments.push("--help"); + let output = run(directory, &arguments); + assert_eq!( + output.status.code(), + Some(0), + "`ocomment {}` did not print help", + arguments.join(" ") + ); + let help = String::from_utf8(output.stdout).unwrap(); + let mut rest = help.as_str(); + while let Some(start) = rest.find("--") { + let tail = &rest[start + 2..]; + let end = tail + .find(|character: char| { + !(character.is_ascii_lowercase() || character.is_ascii_digit() || character == '-') + }) + .unwrap_or(tail.len()); + let name = tail[..end].trim_end_matches('-'); + if name.starts_with(|character: char| character.is_ascii_lowercase()) { + found.insert(format!("--{name}")); + } + rest = tail; + } + let children = subcommand_lines(&help); + for (name, _) in &children { + if name == "help" { + continue; + } + let mut child = path.to_vec(); + child.push(name.as_str()); + long_flags_in_help(directory, &child, found); + } +} + +/// A flag nobody can look up is a flag nobody knows about. The manual page is +/// the reference the `man` subcommand and the release archives both hand out, +/// so every flag `--help` mentions anywhere in the command tree has to have an +/// entry there. +#[test] +fn the_manual_page_documents_every_long_flag() { + let directory = tempfile::tempdir().unwrap(); + let mut flags = BTreeSet::new(); + long_flags_in_help(directory.path(), &[], &mut flags); + assert!( + flags.len() >= 20, + "the help walk stopped finding flags, so this test proves nothing: {flags:?}" + ); + // A walk that stopped at the root would still collect enough flags to look + // healthy, so it is pinned to one flag from each depth it has to reach. + for reached in ["--dry-run", "--sha256"] { + assert!( + flags.contains(reached), + "the help walk never reached `{reached}`, so it is not descending: {flags:?}" + ); + } + let page = fs::read_to_string(shipped("docs/ocomment.1")).unwrap(); + let missing: Vec<&String> = flags + .iter() + .filter(|flag| !page.contains(&roff_option(flag))) + .collect(); + assert!( + missing.is_empty(), + "docs/ocomment.1 has no entry for {missing:?}; regenerate it with `ocomment man`" + ); +} + +/// The manual page is generated from the parser, and the generated bytes are +/// checked in twice: once for `man -l docs/ocomment.1` and once for the release +/// archives. A page that drifted from the binary documents a tool nobody ships. +#[test] +fn the_checked_in_manual_page_is_the_one_the_binary_renders() { + let directory = tempfile::tempdir().unwrap(); + let output = run(directory.path(), &["man"]); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + for path in ["docs/ocomment.1", "release-extras/ocomment.1"] { + let checked_in = fs::read(shipped(path)).unwrap(); + assert!( + checked_in == output.stdout, + "{path} is stale; regenerate it with \ + `python3 tools/release_extras.py --binary rust/target/debug/ocomment` \ + and copy release-extras/ocomment.1 to docs/" + ); + } +} + +/// The completion scripts ship from the same generator and go stale the same +/// way, so they are pinned to the binary too. +#[test] +fn the_checked_in_completions_are_the_ones_the_binary_generates() { + let directory = tempfile::tempdir().unwrap(); + for (shell, path) in [ + ("bash", "release-extras/ocomment.bash"), + ("zsh", "release-extras/_ocomment"), + ("fish", "release-extras/ocomment.fish"), + ("powershell", "release-extras/_ocomment.ps1"), + ("elvish", "release-extras/ocomment.elv"), + ] { + let output = run(directory.path(), &["completions", shell]); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let checked_in = fs::read(shipped(path)).unwrap(); + assert!( + checked_in == output.stdout, + "{path} is stale; regenerate it with \ + `python3 tools/release_extras.py --binary rust/target/debug/ocomment`" + ); + } +} diff --git a/rust/ocomment/tests/source_guards.rs b/rust/ocomment/tests/source_guards.rs new file mode 100644 index 0000000..77aba32 --- /dev/null +++ b/rust/ocomment/tests/source_guards.rs @@ -0,0 +1,208 @@ +//! Guards that read this crate's own source text. +//! +//! A test that runs the binary can only catch a bypass on the paths it +//! happens to exercise. These read the sources instead, so an invariant that +//! holds today cannot be broken quietly by a line added tomorrow. + +use std::{collections::BTreeSet, fs, path::PathBuf}; + +/// Every source file of the crate, embedded at compile time so the scan does +/// not depend on the directory the test runs in. `the_guard_reads_every_source` +/// keeps this list equal to what is on disk. +const SOURCES: [(&str, &str); 10] = [ + ("atomic.rs", include_str!("../src/atomic.rs")), + ("cli.rs", include_str!("../src/cli.rs")), + ("config.rs", include_str!("../src/config.rs")), + ("files.rs", include_str!("../src/files.rs")), + ("git.rs", include_str!("../src/git.rs")), + ("lsp.rs", include_str!("../src/lsp.rs")), + ("main.rs", include_str!("../src/main.rs")), + ("output.rs", include_str!("../src/output.rs")), + ("plugin.rs", include_str!("../src/plugin.rs")), + ("values.rs", include_str!("../src/values.rs")), +]; + +/// The names this crate gives a handle on the program's standard output: the +/// locked writer `output::stdout()` returns is bound as `stdout`, and every +/// function that is handed it takes it as `output`. Nothing else in the crate +/// is written to under either name. +const STDOUT_HANDLES: [&str; 2] = ["stdout", "output"]; + +/// The write macros, matched with their opening parenthesis so the target is +/// the text that follows. +const MACROS: [&str; 2] = ["write!(", "writeln!("]; + +/// The method form of the same write. +const METHOD: &str = ".write_all("; + +/// The call every write to standard output is raised through. +const WRAPPER: &str = "wrote("; + +/// One write whose target is a standard-output handle. +struct StdoutWrite { + line: usize, + /// The call sits directly inside `wrote(` — or `output::wrote(`. + wrapped: bool, +} + +/// Every write to the program's own standard output is raised through +/// [`output::wrote`], which tags a lost reader as `OutputPipeClosed` so `main` +/// can end quietly for that case and only that case. A raw `writeln!` would +/// return a bare `BrokenPipe` that the chain cannot tell apart from a real +/// failure — `git hash-object` dropping the blob it was being handed, say — +/// and `ocomment … | head` would start failing runs, or a failed staged fix +/// would start passing. +/// +/// The invariant checked here is textual: a `write!`, `writeln!`, or +/// `write_all` whose target names a standard-output handle must have `wrote(` +/// immediately in front of it. It is deliberately syntactic rather than +/// semantic — it cannot know what a handle is, only what it is called — so it +/// leans on the naming convention above and on +/// `standard_output_is_locked_in_exactly_one_place`, which keeps a writer from +/// being conjured anonymously under some other name. +#[test] +fn every_write_to_standard_output_goes_through_wrote() { + let mut wrapped = 0; + let mut bare = Vec::new(); + for (name, source) in SOURCES { + for call in stdout_writes(source) { + if call.wrapped { + wrapped += 1; + } else { + bare.push(format!("src/{name}:{}", call.line)); + } + } + } + assert!( + bare.is_empty(), + "these writes to standard output bypass `output::wrote`, so a reader \ + that closed the pipe would surface as an unrecognizable I/O failure: \ + {bare:?}" + ); + // A scan that matches nothing would pass this test forever. + assert!( + wrapped >= 30, + "the guard recognized only {wrapped} writes to standard output, far \ + fewer than the crate makes; the naming convention it reads must have \ + changed, and the guard with it" + ); +} + +/// The handle can only be watched by name if it is only ever made in one +/// place. `output::stdout()` locks standard output for the whole run; nowhere +/// else may turn it into a writer, whether by locking it, writing to it, or +/// flushing it. (Naming it to ask whether it is a terminal is not writing to +/// it, and neither is handing the LSP server its own protocol channel.) +#[test] +fn standard_output_is_locked_in_exactly_one_place() { + let mut offenders = Vec::new(); + for (name, source) in SOURCES { + if name == "output.rs" { + continue; + } + for (at, _) in source.match_indices("stdout()") { + let rest = &source[at + "stdout()".len()..]; + if [".lock()", ".write", ".flush"] + .iter() + .any(|call| rest.starts_with(call)) + { + offenders.push(format!("src/{name}:{}", line_of(source, at))); + } + } + } + assert!( + offenders.is_empty(), + "standard output is written through a handle made outside \ + `output::stdout`, where the pipe guard cannot see it: {offenders:?}" + ); +} + +/// The embedded list is the whole crate, so a module added later is scanned +/// rather than silently exempt. +#[test] +fn the_guard_reads_every_source() { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut on_disk = BTreeSet::new(); + let mut pending = vec![(String::new(), root)]; + while let Some((prefix, directory)) = pending.pop() { + for entry in fs::read_dir(&directory).unwrap() { + let entry = entry.unwrap(); + let name = entry.file_name().to_str().unwrap().to_owned(); + let name = format!("{prefix}{name}"); + if entry.file_type().unwrap().is_dir() { + pending.push((format!("{name}/"), entry.path())); + } else if name.ends_with(".rs") { + on_disk.insert(name); + } + } + } + let scanned: BTreeSet = SOURCES.iter().map(|(name, _)| (*name).to_owned()).collect(); + assert_eq!( + scanned, on_disk, + "the source list this file scans is not the source list on disk" + ); +} + +/// Every write in `source` whose target names a standard-output handle. +fn stdout_writes(source: &str) -> Vec { + let mut writes = Vec::new(); + for marker in MACROS { + for (at, _) in source.match_indices(marker) { + let target = handle(first_argument(&source[at + marker.len()..])); + if STDOUT_HANDLES.contains(&target) { + writes.push(StdoutWrite { + line: line_of(source, at), + wrapped: wraps(&source[..at]), + }); + } + } + } + for (at, _) in source.match_indices(METHOD) { + let receiver = identifier_before(source, at); + if STDOUT_HANDLES.contains(&receiver) { + writes.push(StdoutWrite { + line: line_of(source, at), + wrapped: wraps(&source[..at - receiver.len()]), + }); + } + } + writes +} + +/// Whether the call that follows `prefix` sits directly inside `wrote(`. +fn wraps(prefix: &str) -> bool { + prefix.trim_end().ends_with(WRAPPER) +} + +/// The 1-based line byte `at` falls on. +fn line_of(source: &str, at: usize) -> usize { + source[..at].matches('\n').count() + 1 +} + +/// The first argument of a call, given everything after its opening +/// parenthesis. A target too involved to end at the first comma — a call of +/// its own, say — comes back as something no handle is named, and the write is +/// left to `standard_output_is_locked_in_exactly_one_place`. +fn first_argument(rest: &str) -> &str { + let end = rest.find([',', ')']).unwrap_or(rest.len()); + rest[..end].trim() +} + +/// A target expression reduced to the name it writes through, so that +/// `&mut output` and `output` are the same handle. +fn handle(target: &str) -> &str { + let target = target.trim_start_matches('&').trim_start(); + target.strip_prefix("mut ").unwrap_or(target).trim() +} + +/// The identifier ending at byte `at`, empty when the byte before it is not +/// part of one. +fn identifier_before(source: &str, at: usize) -> &str { + let start = source[..at] + .char_indices() + .rev() + .take_while(|(_, character)| character.is_ascii_alphanumeric() || *character == '_') + .last() + .map_or(at, |(index, _)| index); + &source[start..at] +} From 9b43e4a5b75bd55e67e470314c6b8c594edc2933 Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Wed, 26 Aug 2026 04:30:08 +0900 Subject: [PATCH 04/21] fix(core): make incremental rescans agree with full scans; doctor probes external tools Incremental engine (found by the widened proptest and a new cross-edit soundness property): - rescan the whole remaining suffix and stop at the convergence checkpoint (scan_until_checkpoint) instead of scanning a truncated slice, so lookahead across the cut sees the same bytes as a full scan (dropped `unterminated-string` diagnostics) - one RestartRules predicate shared by emit time and reuse time: reused checkpoints are re-validated against the edited bytes (Python encoding declaration on line 2, C/C++ line splices, CRLF pairs), and the reused tail is rejected while the preamble is unsettled (stale Shebang/Encoding kinds) - deterministic regression tests for each case, a per-language checkpoint soundness property, a cross-edit property, and a token-mixing generator so preamble tokens are reachable CLI: - doctor probes git/curl/gh/oras/cosign with their purposes, prints cwd/root/config trace/TTY state, and sanitises probe output - -q help describes what it really suppresses; fix --dry-run reports skips on stderr so stdout stays a patch; pronoun agreement; init note only after a successful create; actionable plugin overwrite message Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RLSWPMQ9PvtGi3jyBF7bJx --- CHANGELOG.md | 7 + docs/ocomment.1 | 2 +- release-extras/_ocomment | 80 +-- release-extras/_ocomment.ps1 | 80 +-- release-extras/ocomment.1 | 2 +- release-extras/ocomment.elv | 80 +-- release-extras/ocomment.fish | 40 +- rust/ocomment-core/src/incremental.rs | 498 +++++++++++++++++- rust/ocomment-core/src/scanner.rs | 172 +++++- .../tests/properties.proptest-regressions | 1 + rust/ocomment-core/tests/properties.rs | 123 ++++- rust/ocomment/src/cli.rs | 180 +++++-- rust/ocomment/src/output.rs | 92 +++- rust/ocomment/src/plugin.rs | 20 +- rust/ocomment/tests/cli.rs | 384 +++++++++++++- 15 files changed, 1498 insertions(+), 263 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e031869..9f6ceb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,13 @@ All notable changes to OComment will be documented here. The project follows - `-` as a target: `check`, `diff`, and `scan` read standard input under the `` pseudo-path. - `fix --dry-run`, which prints the patch `fix` would apply and writes nothing. + Skipped paths are reported on standard error, so its standard output stays a + patch that `git apply` accepts. +- `ocomment doctor` probes the optional tools OComment shells out to — `curl`, + `gh`, `oras`, and `cosign`, alongside `git` — and reports the environment it + resolved: the working directory, the root, the configuration files it merged, + and whether its output is a terminal. A missing tool is a row in the report + naming what needs it, never a failing run. - `init --force` and `init --stdout`; `init` otherwise refuses to overwrite an existing file and notes a configuration that already applies to the directory. diff --git a/docs/ocomment.1 b/docs/ocomment.1 index 11c6dea..c58cb21 100644 --- a/docs/ocomment.1 +++ b/docs/ocomment.1 @@ -269,7 +269,7 @@ never .RE .TP \fB\-q\fR, \fB\-\-quiet\fR -Print nothing but errors and diagnostics +Drop the run summary and notes; the command\*(Aqs product (findings, patch, listing) is still written .TP \fB\-v\fR, \fB\-\-verbose\fR Trace what is scanned and summarize every comment kind and skipped file diff --git a/release-extras/_ocomment b/release-extras/_ocomment index 2e59b77..45f4fee 100644 --- a/release-extras/_ocomment +++ b/release-extras/_ocomment @@ -82,8 +82,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -171,8 +171,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -252,8 +252,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -332,8 +332,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -412,8 +412,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -490,8 +490,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -567,8 +567,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -647,8 +647,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -725,8 +725,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -803,8 +803,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -880,8 +880,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -968,8 +968,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -1046,8 +1046,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -1124,8 +1124,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -1201,8 +1201,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -1279,8 +1279,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -1357,8 +1357,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -1483,8 +1483,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -1561,8 +1561,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ @@ -1638,8 +1638,8 @@ version-comment\:"A MySQL versioned comment that the server executes"))' \ '--force-invalid[Apply the edits that are still provably safe when the source fails to scan]' \ '--force-protected[Remove protected comments such as shebang and encoding preambles]' \ '--no-preview[Omit the one-line comment text from human \`check\` and \`scan\` lines]' \ -'(-v --verbose)-q[Print nothing but errors and diagnostics]' \ -'(-v --verbose)--quiet[Print nothing but errors and diagnostics]' \ +'(-v --verbose)-q[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ +'(-v --verbose)--quiet[Drop the run summary and notes; the command'\''s product (findings, patch, listing) is still written]' \ '-v[Trace what is scanned and summarize every comment kind and skipped file]' \ '--verbose[Trace what is scanned and summarize every comment kind and skipped file]' \ '-h[Print help (see more with '\''--help'\'')]' \ diff --git a/release-extras/_ocomment.ps1 b/release-extras/_ocomment.ps1 index 907a86d..143f24d 100644 --- a/release-extras/_ocomment.ps1 +++ b/release-extras/_ocomment.ps1 @@ -35,8 +35,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -76,8 +76,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -102,8 +102,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -127,8 +127,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -152,8 +152,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -175,8 +175,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -198,8 +198,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -224,8 +224,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -247,8 +247,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -270,8 +270,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -293,8 +293,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -326,8 +326,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -349,8 +349,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -372,8 +372,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -395,8 +395,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -418,8 +418,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -441,8 +441,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -495,8 +495,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -518,8 +518,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') @@ -541,8 +541,8 @@ Register-ArgumentCompleter -Native -CommandName 'ocomment' -ScriptBlock { [CompletionResult]::new('--force-invalid', '--force-invalid', [CompletionResultType]::ParameterName, 'Apply the edits that are still provably safe when the source fails to scan') [CompletionResult]::new('--force-protected', '--force-protected', [CompletionResultType]::ParameterName, 'Remove protected comments such as shebang and encoding preambles') [CompletionResult]::new('--no-preview', '--no-preview', [CompletionResultType]::ParameterName, 'Omit the one-line comment text from human `check` and `scan` lines') - [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') - [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Print nothing but errors and diagnostics') + [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') + [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Trace what is scanned and summarize every comment kind and skipped file') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') diff --git a/release-extras/ocomment.1 b/release-extras/ocomment.1 index 11c6dea..c58cb21 100644 --- a/release-extras/ocomment.1 +++ b/release-extras/ocomment.1 @@ -269,7 +269,7 @@ never .RE .TP \fB\-q\fR, \fB\-\-quiet\fR -Print nothing but errors and diagnostics +Drop the run summary and notes; the command\*(Aqs product (findings, patch, listing) is still written .TP \fB\-v\fR, \fB\-\-verbose\fR Trace what is scanned and summarize every comment kind and skipped file diff --git a/release-extras/ocomment.elv b/release-extras/ocomment.elv index 2c36aa9..c1d3421 100644 --- a/release-extras/ocomment.elv +++ b/release-extras/ocomment.elv @@ -32,8 +32,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -72,8 +72,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -97,8 +97,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -121,8 +121,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -145,8 +145,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -167,8 +167,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -189,8 +189,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -214,8 +214,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -236,8 +236,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -258,8 +258,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -280,8 +280,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -312,8 +312,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -334,8 +334,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -356,8 +356,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -378,8 +378,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -400,8 +400,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -422,8 +422,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -467,8 +467,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -489,8 +489,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' @@ -511,8 +511,8 @@ set edit:completion:arg-completer[ocomment] = {|@words| cand --force-invalid 'Apply the edits that are still provably safe when the source fails to scan' cand --force-protected 'Remove protected comments such as shebang and encoding preambles' cand --no-preview 'Omit the one-line comment text from human `check` and `scan` lines' - cand -q 'Print nothing but errors and diagnostics' - cand --quiet 'Print nothing but errors and diagnostics' + cand -q 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' + cand --quiet 'Drop the run summary and notes; the command''s product (findings, patch, listing) is still written' cand -v 'Trace what is scanned and summarize every comment kind and skipped file' cand --verbose 'Trace what is scanned and summarize every comment kind and skipped file' cand -h 'Print help (see more with ''--help'')' diff --git a/release-extras/ocomment.fish b/release-extras/ocomment.fish index a29b8b9..5092ea3 100644 --- a/release-extras/ocomment.fish +++ b/release-extras/ocomment.fish @@ -101,7 +101,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_needs_command" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_needs_command" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_needs_command" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_needs_command" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_needs_command" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_needs_command" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_needs_command" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_needs_command" -s V -l version -d 'Print version' @@ -198,7 +198,7 @@ complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l index-only - complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand check" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand check" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand check" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand check" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -281,7 +281,7 @@ complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l dry-run -d 'Pr complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -363,7 +363,7 @@ complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l index-only -d complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -445,7 +445,7 @@ complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l index-only -d complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -525,7 +525,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -605,7 +605,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -688,7 +688,7 @@ complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l stdout -d 'Pr complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand init" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand init" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand init" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand init" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -768,7 +768,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand config" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand config" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand config" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand config" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -848,7 +848,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -928,7 +928,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -f -a "add" -d 'Install a plugin and pin its digest in .ocomment.lock' @@ -1018,7 +1018,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -1098,7 +1098,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -1178,7 +1178,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -1258,7 +1258,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -1338,7 +1338,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -1418,7 +1418,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from help" -f -a "add" -d 'Install a plugin and pin its digest in .ocomment.lock' @@ -1505,7 +1505,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -1585,7 +1585,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l config -d 'Read this configuration file instead of discovering `.ocomment.toml`' -r -F @@ -1665,7 +1665,7 @@ never\t''" complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l force-invalid -d 'Apply the edits that are still provably safe when the source fails to scan' complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l force-protected -d 'Remove protected comments such as shebang and encoding preambles' complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l no-preview -d 'Omit the one-line comment text from human `check` and `scan` lines' -complete -c ocomment -n "__fish_ocomment_using_subcommand man" -s q -l quiet -d 'Print nothing but errors and diagnostics' +complete -c ocomment -n "__fish_ocomment_using_subcommand man" -s q -l quiet -d 'Drop the run summary and notes; the command\'s product (findings, patch, listing) is still written' complete -c ocomment -n "__fish_ocomment_using_subcommand man" -s v -l verbose -d 'Trace what is scanned and summarize every comment kind and skipped file' complete -c ocomment -n "__fish_ocomment_using_subcommand man" -s h -l help -d 'Print help (see more with \'--help\')' complete -c ocomment -n "__fish_ocomment_using_subcommand help; and not __fish_seen_subcommand_from check fix diff scan strip lsp init config languages plugin completions doctor man help" -f -a "check" -d 'Report removable comments (default command)' diff --git a/rust/ocomment-core/src/incremental.rs b/rust/ocomment-core/src/incremental.rs index f265b36..01e454c 100644 --- a/rust/ocomment-core/src/incremental.rs +++ b/rust/ocomment-core/src/incremental.rs @@ -1,6 +1,8 @@ use crate::{ ByteSpan, Language, Layout, ScanOptions, ScanReport, Severity, TransformOptions, - TransformResult, scanner::scan_with_checkpoints, transform::transform_report, + TransformResult, + scanner::{RestartRules, preamble_is_settled, scan_until_checkpoint, scan_with_checkpoints}, + transform::transform_report, }; use thiserror::Error; @@ -144,18 +146,36 @@ impl IncrementalDocument { let safe_start = if !can_reuse { 0 } else { - self.safe_checkpoints + // The checkpoints belong to the *previous* revision, and a + // checkpoint is only a restart point while the bytes around it + // still allow one: an edit that turns line 2 into a Python encoding + // declaration, or that splices two C lines together, withdraws that + // permission. Every candidate is therefore re-asked against the + // edited document, falling back to an earlier checkpoint and + // ultimately to a full scan. + let rules = RestartRules::of(&next, self.language); + let usable = self + .safe_checkpoints + .partition_point(|point| *point <= earliest); + self.safe_checkpoints[..usable] .iter() .copied() - .take_while(|point| *point <= earliest) - .last() + .rev() + .find(|point| rules.permit_restart_at(&next, *point)) .unwrap_or(0) }; let old_convergence = if can_reuse { - self.safe_checkpoints - .iter() - .copied() - .find(|point| *point >= old_tail_start.max(safe_start)) + // Converging keeps the previous revision's report for every byte + // past the convergence point, shifted by the edit's length delta — + // including each comment's kind. Only the preamble rules care where + // a comment sits, so the tail may be reused exactly while it lies + // past the preamble both where it was and where the edit moves it; + // otherwise the scan runs on to the first checkpoint that does. + self.safe_checkpoints.iter().copied().find(|point| { + *point >= old_tail_start.max(safe_start) + && preamble_is_settled(&self.source, *point) + && preamble_is_settled(&next, new_tail_start + point - old_tail_start) + }) } else { None }; @@ -163,15 +183,24 @@ impl IncrementalDocument { let mut partial = None; if let Some(old_convergence) = old_convergence { let new_convergence = new_tail_start + old_convergence - old_tail_start; - let (report, checkpoints) = scan_with_checkpoints( - &next[safe_start..new_convergence], + // The scanner is handed the whole suffix, never a slice cut at the + // convergence point: lexical lookahead that reaches past the cut + // would otherwise decide differently than it does in the real + // document and the rescan would lose comments or diagnostics. + let (report, checkpoints, converged) = scan_until_checkpoint( + &next[safe_start..], self.language, self.options.clone(), safe_start, + new_convergence, ); - if checkpoints.last().copied() == Some(new_convergence) { + if converged { reused_tail = Some((old_convergence, new_convergence)); partial = Some((report, checkpoints, new_convergence)); + } else { + // Lexical state diverged, so the scan already ran to the end of + // the suffix; that report is exactly the fallback. + partial = Some((report, checkpoints, next.len())); } } let (mut suffix, suffix_checkpoints, rescan_end) = partial.unwrap_or_else(|| { @@ -187,7 +216,7 @@ impl IncrementalDocument { .report .comments .iter() - .take_while(|comment| comment.span.end <= safe_start) + .take_while(|comment| comment.span.start < safe_start && comment.span.end <= safe_start) .cloned() .collect(); comments.append(&mut suffix.comments); @@ -209,7 +238,9 @@ impl IncrementalDocument { .report .diagnostics .iter() - .take_while(|diagnostic| diagnostic.span.end <= safe_start) + .take_while(|diagnostic| { + diagnostic.span.start < safe_start && diagnostic.span.end <= safe_start + }) .cloned() .collect(); diagnostics.append(&mut suffix.diagnostics); @@ -352,6 +383,418 @@ fn line_checkpoints(source: &[u8]) -> Vec { #[cfg(test)] mod tests { use super::*; + use crate::scanner::scan_with_checkpoints; + use proptest::prelude::*; + + /// Bytes that exercise every built-in scanner's string, comment, heredoc + /// and template states rather than only the C-family delimiters. + fn lexical_byte() -> impl Strategy { + prop_oneof![ + 4 => any::(), + 2 => Just(b'\n'), + 1 => Just(b'\r'), + 1 => Just(b'/'), + 1 => Just(b'*'), + 1 => Just(b'\''), + 1 => Just(b'"'), + 1 => Just(b'#'), + 1 => Just(b'`'), + 1 => Just(b'{'), + 1 => Just(b'}'), + 1 => Just(b'<'), + 1 => Just(b'>'), + 1 => Just(b'='), + 1 => Just(b'['), + 1 => Just(b']'), + 1 => Just(b'-'), + 1 => Just(b'|'), + 1 => Just(b'?'), + 1 => Just(b'\\'), + 1 => Just(b'$'), + 1 => Just(b'%'), + 1 => Just(b'('), + 1 => Just(b')'), + 1 => Just(b'@'), + 1 => Just(b':'), + 1 => Just(b'!'), + 1 => Just(b'~'), + ] + } + + /// Multi-byte tokens a single-byte alphabet can never synthesise. The + /// preamble and directive rules only fire on whole words, so without these + /// the generated sources never reach the code paths that make a checkpoint + /// depend on the bytes in front of it. + fn lexical_fragment() -> impl Strategy> { + prop_oneof![ + 8 => lexical_byte().prop_map(|byte| vec![byte]), + 1 => Just(b"coding:".to_vec()), + 1 => Just(b"# -*- coding: utf-8 -*-".to_vec()), + 1 => Just(b"# coding: latin-1".to_vec()), + 1 => Just(b"#!".to_vec()), + 1 => Just(b"//go:build".to_vec()), + 1 => Just(b"/*#__PURE__*/".to_vec()), + 1 => Just(b"") { index + 4 + relative + 3 @@ -1757,6 +1808,82 @@ fn classify_comment( lexical } +/// The document-wide half of the restart rules: C and C++ splice +/// `\` out of the input before lexing, and the remapped copy that +/// results is scanned without tracking checkpoints, so a full scan of a spliced +/// document offers no restart point beyond offset 0. +fn line_splicing_permits_restarts(source: &[u8], language: Language) -> bool { + !matches!(language, Language::C | Language::Cpp) || !contains_line_splice(source) +} + +/// A checkpoint sits immediately after a line terminator, and a CRLF pair is a +/// single terminator. An edit that supplies the LF after an existing CR moves +/// the boundary one byte on, leaving the offset a previous revision recorded +/// inside the pair, where no scan of these bytes would ever resume. +fn the_line_ending_permits_a_restart(source: &[u8], offset: usize) -> bool { + offset == 0 || source.get(offset - 1) != Some(&b'\r') || source.get(offset) != Some(&b'\n') +} + +/// Preamble classification depends on the absolute offset, and Python only +/// recognises an encoding declaration while scanning from offset 0, which makes +/// the start of line 2 a restart point exactly when no encoding declaration +/// follows. Offset 0 always passes — restarting a scan there *is* the full +/// scan. +fn the_preamble_permits_a_restart(source: &[u8], language: Language, offset: usize) -> bool { + offset == 0 + || language != Language::Python + || !is_within_first_two_lines(source, offset) + || !python_line_declares_encoding(source, offset) +} + +/// The restart rules for one revision of a document: a safe checkpoint promises +/// that restarting the scan there reproduces the rest of a full scan byte for +/// byte, and both halves of that promise are conditions on the bytes *around* +/// the checkpoint. The document-wide half is answered once here, when the rules +/// are built, because answering it costs a scan of the source. +/// +/// The scanner consults these rules before emitting a checkpoint; the +/// incremental engine builds them for the *edited* bytes and consults them +/// again before restarting at a checkpoint the previous revision recorded. +/// Emitting a checkpoint and reusing one therefore ask one function and cannot +/// drift apart. +#[derive(Clone, Copy)] +pub(crate) struct RestartRules { + language: Language, + splicing_permits_restarts: bool, +} + +impl RestartRules { + pub(crate) fn of(source: &[u8], language: Language) -> Self { + Self { + language, + splicing_permits_restarts: line_splicing_permits_restarts(source, language), + } + } + + /// Whether restarting a scan of `source` — the bytes these rules were built + /// from — at `offset` reproduces the rest of a full scan of it. + pub(crate) fn permit_restart_at(&self, source: &[u8], offset: usize) -> bool { + self.splicing_permits_restarts + && the_line_ending_permits_a_restart(source, offset) + && the_preamble_permits_a_restart(source, self.language, offset) + } +} + +/// Whether classification from `offset` onwards is independent of where those +/// bytes sit in the document. The preamble rules are the only position +/// sensitive ones — a `#!` line is a shebang only at offset 0, and a Python +/// encoding declaration only inside the first two lines — so anything past the +/// first two lines classifies the same wherever an edit moves it to. +/// +/// The incremental engine reuses the previous revision's report for the tail it +/// converges on, shifted by the edit's length delta. That reuse keeps the old +/// classification, so it is sound exactly while the tail is settled at both its +/// old and its new position. +pub(crate) fn preamble_is_settled(source: &[u8], offset: usize) -> bool { + !is_within_first_two_lines(source, offset) +} + fn is_within_first_two_lines(source: &[u8], offset: usize) -> bool { let mut line_breaks = 0; let mut index = 0; @@ -1779,6 +1906,21 @@ fn is_within_first_two_lines(source: &[u8], offset: usize) -> bool { true } +/// Whether the line beginning at `line_start` carries a Python encoding +/// declaration, and therefore a comment whose classification depends on the +/// scan starting at offset 0. +fn python_line_declares_encoding(source: &[u8], line_start: usize) -> bool { + let mut index = line_start; + while matches!(source.get(index), Some(b' ' | b'\t' | 0x0c)) { + index += 1; + } + if source.get(index) != Some(&b'#') { + return false; + } + let end = line_end(source, index + 1); + is_python_encoding_declaration(source, index, &source[index..end]) +} + fn is_python_encoding_declaration(source: &[u8], start: usize, raw: &[u8]) -> bool { if !is_within_first_two_lines(source, start) || !raw.starts_with(b"#") { return false; diff --git a/rust/ocomment-core/tests/properties.proptest-regressions b/rust/ocomment-core/tests/properties.proptest-regressions index db46a2a..357568a 100644 --- a/rust/ocomment-core/tests/properties.proptest-regressions +++ b/rust/ocomment-core/tests/properties.proptest-regressions @@ -5,3 +5,4 @@ # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. cc 82a94048b80c617d96de131fb906d6ccbd0ad11ce86ba8a5020e6a241aad96a6 # shrinks to source = [0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 39, 128, 10, 34, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], replacement = [], first = 7458001845501105093, second = 8922367292645105975 +cc 139e96148e557b3279c30b38950e0c1adfdea4d3e05d82059df8362c86852ad0 # shrinks to source = [0, 0, 35, 0, 39, 128, 34, 39, 10, 39, 0, 35, 35, 0, 0, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], replacement = [128], first = 14929730563465487330, second = 5900713899202105475 diff --git a/rust/ocomment-core/tests/properties.rs b/rust/ocomment-core/tests/properties.rs index 467ae9a..aeaeb62 100644 --- a/rust/ocomment-core/tests/properties.rs +++ b/rust/ocomment-core/tests/properties.rs @@ -4,6 +4,64 @@ use ocomment_core::{ }; use proptest::prelude::*; +/// Bytes that exercise every built-in scanner's string, comment, heredoc and +/// template states rather than only the C-family delimiters. +fn lexical_byte() -> impl Strategy { + prop_oneof![ + 4 => any::(), + 2 => Just(b'\n'), + 1 => Just(b'\r'), + 1 => Just(b'/'), + 1 => Just(b'*'), + 1 => Just(b'\''), + 1 => Just(b'"'), + 1 => Just(b'#'), + 1 => Just(b'`'), + 1 => Just(b'{'), + 1 => Just(b'}'), + 1 => Just(b'<'), + 1 => Just(b'>'), + 1 => Just(b'='), + 1 => Just(b'['), + 1 => Just(b']'), + 1 => Just(b'-'), + 1 => Just(b'|'), + 1 => Just(b'?'), + 1 => Just(b'\\'), + 1 => Just(b'$'), + 1 => Just(b'%'), + 1 => Just(b'('), + 1 => Just(b')'), + 1 => Just(b'@'), + 1 => Just(b':'), + 1 => Just(b'!'), + 1 => Just(b'~'), + ] +} + +/// Multi-byte tokens a single-byte alphabet can never synthesise. The preamble +/// and directive rules only fire on whole words, so without these the generated +/// sources never reach the code paths that make a scan depend on where in the +/// document it starts. +fn lexical_fragment() -> impl Strategy> { + prop_oneof![ + 8 => lexical_byte().prop_map(|byte| vec![byte]), + 1 => Just(b"coding:".to_vec()), + 1 => Just(b"# -*- coding: utf-8 -*-".to_vec()), + 1 => Just(b"# coding: latin-1".to_vec()), + 1 => Just(b"#!".to_vec()), + 1 => Just(b"//go:build".to_vec()), + 1 => Just(b"/*#__PURE__*/".to_vec()), + 1 => Just(b"\n".as_slice()), + (Language::Shell, b"#!/bin/sh\n# ordinary\n".as_slice()), + ( + Language::Python, + b"# -*- coding: utf-8 -*-\n# ordinary\n".as_slice(), + ), + ( + Language::Sql, + b"/*+ INDEX(t idx) */\n/*!40000 ALTER TABLE t */\n-- ordinary\n".as_slice(), + ), + ] +} + +/// Each variant steers at least one branch of the table: the policies, the +/// preamble override, both kind lists and both regex lists, plus the overlap +/// where a keep and a remove pattern match the same bytes. +fn option_variants() -> Vec { + let mut variants = Vec::new(); + for policy in Policy::ALL { + for force_protected in [false, true] { + let base = ScanOptions { + policy, + force_protected, + ..Default::default() + }; + variants.push(base.clone()); + variants.push(ScanOptions { + keep_kinds: vec![CommentKind::Line, CommentKind::HtmlComment], + ..base.clone() + }); + variants.push(ScanOptions { + remove_kinds: vec![ + CommentKind::License, + CommentKind::Directive, + CommentKind::Shebang, + CommentKind::Encoding, + ], + ..base.clone() + }); + variants.push(ScanOptions { + keep_regex: vec!["never".into(), "(?i)ordinary".into()], + ..base.clone() + }); + variants.push(ScanOptions { + remove_regex: vec!["(?i)copyright".into(), "(?i)doc".into()], + ..base.clone() + }); + variants.push(ScanOptions { + keep_regex: vec!["(?i)coding".into()], + remove_regex: vec!["(?i)coding".into()], + ..base + }); + } + } + variants +} + +fn explain( + kind: CommentKind, + raw: &str, + language: Language, + options: &ScanOptions, +) -> DispositionExplanation { + explain_disposition(kind, raw.as_bytes(), language, options) +} + +/// The pattern sets are the same for every comment scanned under one set of +/// options, so a report that explains a whole file compiles them once and calls +/// the precompiled form. That form has to be the very same answer, over the +/// whole branch table, or the cheap path would quietly explain something else. +#[test] +fn the_precompiled_explanation_equals_the_convenience_wrapper() { + for options in option_variants() { + let patterns = DispositionPatterns::compile(&options).expect("the fixtures compile"); + for (language, source) in fixtures() { + let report = scan(source, language, options.clone()); + for comment in &report.comments { + let raw = &source[comment.span.start..comment.span.end]; + assert_eq!( + explain_disposition_with(&patterns, comment.kind, raw, language, &options), + explain_disposition(comment.kind, raw, language, &options), + "{language} {} `{}` under {options:?}", + comment.kind, + String::from_utf8_lossy(raw), + ); + } + } + } +} + +/// A pattern list that will not compile is ignored by the scanner, and both +/// entry points ignore it the same way: the empty sets a caller compiles for +/// the precompiled form are the fallback the wrapper builds for itself. +#[test] +fn an_unparseable_pattern_list_falls_back_the_same_way() { + let options = ScanOptions { + keep_regex: vec!["(".into()], + ..ScanOptions::default() + }; + assert!(DispositionPatterns::compile(&options).is_err()); + let raw = b"// ordinary".as_slice(); + assert_eq!( + explain_disposition_with( + &DispositionPatterns::empty(), + CommentKind::Line, + raw, + Language::Rust, + &options, + ), + explain_disposition(CommentKind::Line, raw, Language::Rust, &options), + ); +} + +#[test] +fn every_kind_is_covered_by_a_fixture() { + let mut seen = BTreeSet::new(); + for (language, source) in fixtures() { + let report = scan(source, language, ScanOptions::default()); + assert!(report.valid, "{language} fixture must lex cleanly"); + for comment in &report.comments { + seen.insert(comment.kind.as_str()); + } + } + let expected: BTreeSet<_> = CommentKind::ALL.iter().map(|kind| kind.as_str()).collect(); + assert_eq!(seen, expected, "fixtures must exercise every comment kind"); +} + +#[test] +fn explanations_agree_with_the_scanner_over_the_whole_branch_table() { + for options in option_variants() { + for (language, source) in fixtures() { + let report = scan(source, language, options.clone()); + for comment in &report.comments { + let raw = &source[comment.span.start..comment.span.end]; + let explanation = explain_disposition(comment.kind, raw, language, &options); + assert_eq!( + explanation.action().is_remove(), + comment.disposition.is_remove(), + "{language} {} `{}` under {options:?}: {explanation} contradicts {}", + comment.kind, + String::from_utf8_lossy(raw), + comment.disposition, + ); + } + } + } +} + +#[test] +fn an_invalid_regex_explains_the_same_way_the_scanner_scans() { + let options = ScanOptions { + keep_regex: vec!["(unclosed".into()], + ..Default::default() + }; + let source = b"// plain\n"; + let report = scan(source, Language::Rust, options.clone()); + assert!(!report.valid, "an invalid pattern is a scan error"); + let explanation = explain(CommentKind::Line, "// plain", Language::Rust, &options); + assert_eq!( + explanation, + DispositionExplanation::RemovedByDefault(Policy::Safe) + ); + assert_eq!( + explanation.action().is_remove(), + report.comments[0].disposition.is_remove(), + ); +} + +#[test] +fn a_kept_kind_names_the_kind() { + let options = ScanOptions { + keep_kinds: vec![CommentKind::Block], + ..Default::default() + }; + let explanation = explain( + CommentKind::Block, + "/* keep me */", + Language::Rust, + &options, + ); + assert_eq!( + explanation, + DispositionExplanation::KeptByKind(CommentKind::Block) + ); + assert_eq!(explanation.action(), Action::Keep); + let sentence = explanation.to_string(); + assert!(sentence.starts_with("kept:"), "{sentence}"); + assert!(sentence.contains("block"), "{sentence}"); +} + +#[test] +fn a_kept_regex_names_the_first_matching_pattern() { + let options = ScanOptions { + keep_regex: vec!["never".into(), "(?i)generated".into(), "gener".into()], + ..Default::default() + }; + let explanation = explain( + CommentKind::Line, + "// GENERATED by a tool", + Language::Rust, + &options, + ); + assert_eq!( + explanation, + DispositionExplanation::KeptByRegex { + index: 1, + pattern: "(?i)generated".into(), + } + ); + let sentence = explanation.to_string(); + assert!(sentence.contains("(?i)generated"), "{sentence}"); + assert!(sentence.contains("keep_regex"), "{sentence}"); +} + +#[test] +fn a_removed_regex_names_the_first_matching_pattern() { + let options = ScanOptions { + remove_regex: vec!["nope".into(), "(?i)todo".into()], + ..Default::default() + }; + let explanation = explain( + CommentKind::License, + "// TODO: copyright 2024", + Language::Rust, + &options, + ); + assert_eq!( + explanation, + DispositionExplanation::RemovedByRegex { + index: 1, + pattern: "(?i)todo".into(), + } + ); + assert_eq!(explanation.action(), Action::Remove); + let sentence = explanation.to_string(); + assert!(sentence.starts_with("removed:"), "{sentence}"); + assert!(sentence.contains("(?i)todo"), "{sentence}"); + assert!(sentence.contains("remove_regex"), "{sentence}"); +} + +#[test] +fn a_removed_kind_names_the_kind() { + let options = ScanOptions { + remove_kinds: vec![CommentKind::DocLine], + ..Default::default() + }; + let explanation = explain(CommentKind::DocLine, "/// docs", Language::Rust, &options); + assert_eq!( + explanation, + DispositionExplanation::RemovedByKind(CommentKind::DocLine) + ); + assert!(explanation.to_string().contains("doc-line")); +} + +#[test] +fn the_preamble_is_protected_until_it_is_forced() { + let default = ScanOptions::default(); + assert_eq!( + explain(CommentKind::Shebang, "#!/bin/sh", Language::Shell, &default), + DispositionExplanation::ProtectedPreamble + ); + assert_eq!( + explain( + CommentKind::Encoding, + "# -*- coding: utf-8 -*-", + Language::Python, + &default + ), + DispositionExplanation::ProtectedPreamble + ); + let forced = ScanOptions { + force_protected: true, + ..Default::default() + }; + assert_eq!( + explain(CommentKind::Shebang, "#!/bin/sh", Language::Shell, &forced), + DispositionExplanation::RemovedByDefault(Policy::Safe) + ); + let forced_all = ScanOptions { + force_protected: true, + policy: Policy::All, + ..Default::default() + }; + assert_eq!( + explain( + CommentKind::Shebang, + "#!/bin/sh", + Language::Shell, + &forced_all + ), + DispositionExplanation::RemovedByPolicy(Policy::All) + ); +} + +#[test] +fn a_keep_override_outranks_every_later_branch() { + let options = ScanOptions { + policy: Policy::All, + force_protected: true, + keep_kinds: vec![CommentKind::Shebang], + remove_kinds: vec![CommentKind::Shebang], + remove_regex: vec!["bin".into()], + ..Default::default() + }; + assert_eq!( + explain(CommentKind::Shebang, "#!/bin/sh", Language::Shell, &options), + DispositionExplanation::KeptByKind(CommentKind::Shebang) + ); + let by_regex = ScanOptions { + policy: Policy::All, + keep_regex: vec!["(?i)license".into()], + remove_kinds: vec![CommentKind::License], + ..Default::default() + }; + assert_eq!( + explain( + CommentKind::License, + "// SPDX-License-Identifier: MIT", + Language::Rust, + &by_regex + ), + DispositionExplanation::KeptByRegex { + index: 0, + pattern: "(?i)license".into(), + } + ); +} + +#[test] +fn a_remove_override_outranks_the_policy_protections() { + let options = ScanOptions { + policy: Policy::Legal, + remove_kinds: vec![CommentKind::License, CommentKind::HtmlComment], + ..Default::default() + }; + assert_eq!( + explain( + CommentKind::License, + "// Copyright 2024 Example", + Language::Rust, + &options + ), + DispositionExplanation::RemovedByKind(CommentKind::License) + ); + assert_eq!( + explain( + CommentKind::HtmlComment, + "", + Language::Html, + &options + ), + DispositionExplanation::RemovedByKind(CommentKind::HtmlComment) + ); +} + +#[test] +fn policy_all_removes_what_the_other_policies_protect() { + let options = ScanOptions { + policy: Policy::All, + ..Default::default() + }; + for (kind, raw, language) in [ + ( + CommentKind::HtmlComment, + "", + Language::Html, + ), + ( + CommentKind::Directive, + "// eslint-disable-next-line", + Language::JavaScript, + ), + ( + CommentKind::License, + "// Copyright 2024 Example", + Language::Rust, + ), + ] { + let explanation = explain(kind, raw, language, &options); + assert_eq!( + explanation, + DispositionExplanation::RemovedByPolicy(Policy::All), + "{kind} under policy all" + ); + assert!(explanation.to_string().contains("all")); + } +} + +#[test] +fn html_comments_are_kept_by_both_conservative_policies() { + for policy in [Policy::Safe, Policy::Legal] { + let options = ScanOptions { + policy, + ..Default::default() + }; + let explanation = explain( + CommentKind::HtmlComment, + "", + Language::Html, + &options, + ); + assert_eq!(explanation, DispositionExplanation::KeptHtml); + assert_eq!(explanation.action(), Action::Keep); + assert!(explanation.to_string().contains("HTML")); + } +} + +#[test] +fn a_kept_directive_names_the_matched_directive() { + for (raw, language, name) in [ + ( + "// eslint-disable-next-line", + Language::JavaScript, + Some("eslint"), + ), + ("//go:generate stringer", Language::Go, Some("go:")), + ("// rustfmt::skip", Language::Rust, Some("rustfmt::")), + ( + "/// ", + Language::TypeScript, + Some("///"), + ), + ] { + let explanation = explain( + CommentKind::Directive, + raw, + language, + &ScanOptions::default(), + ); + assert_eq!( + explanation, + DispositionExplanation::KeptDirective { + kind: CommentKind::Directive, + name, + }, + "{raw}" + ); + let sentence = explanation.to_string(); + assert!(sentence.contains("directive"), "{sentence}"); + assert!(sentence.contains(name.expect("named")), "{sentence}"); + } +} + +#[test] +fn an_unnamed_directive_kind_still_explains_itself() { + for (kind, raw) in [ + (CommentKind::OptimizerHint, "/*+ INDEX(t idx) */"), + (CommentKind::VersionComment, "/*!40000 ALTER TABLE t */"), + ] { + let explanation = explain(kind, raw, Language::Sql, &ScanOptions::default()); + assert_eq!( + explanation, + DispositionExplanation::KeptDirective { kind, name: None }, + "{raw}" + ); + let sentence = explanation.to_string(); + assert!(sentence.contains("directive"), "{sentence}"); + assert!(sentence.contains(kind.as_str()), "{sentence}"); + } +} + +#[test] +fn a_license_is_kept_only_by_the_legal_policy_and_names_its_marker() { + let legal = ScanOptions { + policy: Policy::Legal, + ..Default::default() + }; + for (raw, marker) in [ + ("// Copyright 2024 Example", Some("copyright")), + ( + "// SPDX-License-Identifier: MIT", + Some("spdx-license-identifier"), + ), + ("/* All Rights Reserved */", Some("all rights reserved")), + ] { + let explanation = explain(CommentKind::License, raw, Language::Rust, &legal); + assert_eq!( + explanation, + DispositionExplanation::KeptLicense { marker }, + "{raw}" + ); + let sentence = explanation.to_string(); + assert!(sentence.contains(marker.expect("named")), "{sentence}"); + } + let safe = ScanOptions::default(); + let explanation = explain( + CommentKind::License, + "// Copyright 2024 Example", + Language::Rust, + &safe, + ); + assert_eq!( + explanation, + DispositionExplanation::RemovedByDefault(Policy::Safe) + ); + assert!(explanation.to_string().contains("safe")); +} + +#[test] +fn ordinary_comments_fall_through_to_the_policy_default() { + for policy in [Policy::Safe, Policy::Legal] { + let options = ScanOptions { + policy, + ..Default::default() + }; + for (kind, raw) in [ + (CommentKind::Line, "// plain"), + (CommentKind::Block, "/* block */"), + (CommentKind::DocLine, "/// doc line"), + (CommentKind::DocBlock, "/** doc block */"), + ] { + let explanation = explain(kind, raw, Language::Rust, &options); + assert_eq!( + explanation, + DispositionExplanation::RemovedByDefault(policy), + "{kind} under policy {policy}" + ); + assert_eq!(explanation.action(), Action::Remove); + } + } +} + +#[test] +fn the_action_helper_is_the_inverse_of_a_removal() { + assert!(Action::Remove.is_remove()); + assert!(!Action::Keep.is_remove()); + assert_eq!(Action::Keep.as_str(), "keep"); + assert_eq!(Action::Remove.as_str(), "remove"); + assert_eq!(Action::Keep.to_string(), "keep"); + assert_eq!(Action::Remove.to_string(), "remove"); +} + +#[test] +fn explaining_a_report_leaves_the_report_alone() { + let options = ScanOptions { + policy: Policy::Legal, + keep_regex: vec!["(?i)ordinary".into()], + ..Default::default() + }; + for (language, source) in fixtures() { + let before = scan(source, language, options.clone()); + for comment in &before.comments { + let raw = &source[comment.span.start..comment.span.end]; + let _ = explain_disposition(comment.kind, raw, language, &options).to_string(); + } + let after = scan(source, language, options.clone()); + assert_eq!(before, after, "{language} scan output must be untouched"); + } +} diff --git a/rust/ocomment/src/cli.rs b/rust/ocomment/src/cli.rs index 38ab9dc..6ea2ea9 100644 --- a/rust/ocomment/src/cli.rs +++ b/rust/ocomment/src/cli.rs @@ -1,8 +1,9 @@ use crate::{ atomic::{WritePlan, apply_transaction}, - config, files, git, lsp, + config, files, git, interactive, lsp, output::{ - self, Operation, OutputFormat, Presentation, ProcessedFile, RenderOptions, Verbosity, + self, Explanations, FileExplanation, Operation, OutputFormat, Presentation, ProcessedFile, + RenderOptions, Verbosity, }, plugin, values::{CommentKindArg, DialectArg, LanguageArg, LayoutArg, PolicyArg}, @@ -40,7 +41,7 @@ FILES EXAMPLES ocomment - Check the current repository and report removable comments. + Check the current directory and report removable comments. ocomment fix --policy all --layout compact src Remove every comment under src and close the gaps it leaves. ocomment strip --language rust < before.rs > after.rs @@ -78,7 +79,7 @@ User configuration, merged over the built\-in defaults. .SH EXAMPLES .TP .B ocomment -Check the current repository and report removable comments. +Check the current directory and report removable comments. .TP .B ocomment fix \-\-policy all \-\-layout compact src Remove every comment under src and close the gaps it leaves. @@ -211,6 +212,9 @@ struct OutputArgs { /// Omit the one-line comment text from human `check` and `scan` lines. #[arg(long, global = true)] no_preview: bool, + /// List every comment human `check` and `scan` met and name the rule and setting behind each one. + #[arg(long, global = true)] + explain: bool, /// When to draw the live scanning counter on standard error. #[arg(long, global = true, value_enum, default_value_t, value_name = "WHEN")] progress: AutoChoice, @@ -327,6 +331,13 @@ struct FixArgs { /// Print the patch `fix` would apply and write nothing. #[arg(long)] dry_run: bool, + /// Ask about each comment in turn and remove only the accepted ones. + /// + /// The index has no working-tree line to show a hunk from, `--dry-run` + /// writes nothing whatever the answers were, and `-q` asks for a run with + /// no commentary at all. None of the three can also be a conversation. + #[arg(short = 'i', long, conflicts_with_all = ["staged", "dry_run", "quiet"])] + interactive: bool, } impl FixArgs { @@ -425,9 +436,52 @@ enum PluginCommand { }, } +/// The `fix` variants that change what a run does with what it found. Every +/// other command runs with neither. +#[derive(Clone, Copy, Default)] +struct RunFlags { + /// The run produces the patch `fix` would apply and writes nothing. + dry_run: bool, + /// The run asks about each comment before removing it. + interactive: bool, +} + +impl RunFlags { + const NONE: Self = Self { + dry_run: false, + interactive: false, + }; + const DRY_RUN: Self = Self { + dry_run: true, + interactive: false, + }; + const INTERACTIVE: Self = Self { + dry_run: false, + interactive: true, + }; +} + pub fn run() -> Result { let cli = Cli::parse(); let common = cli.common; + // The machine formats are schemas rather than prose, and none of them has + // a place to put an explanation; the JSON one is closed to extension by + // design. So the combination is refused instead of quietly doing nothing. + if common.output.explain && common.output.format != OutputFormat::Human { + bail!("--explain is only available with --format human"); + } + // The flag annotates a report of comments, and only `check` and `scan` + // write one: `fix` reports the files it rewrote, `diff` writes a patch, + // and `strip` writes the stripped source. `--explain` is global, so the + // combination is refused rather than quietly doing nothing. + if common.output.explain + && matches!( + cli.command, + Some(Command::Fix(_) | Command::Diff(_) | Command::Strip) + ) + { + bail!("--explain is only available with `check` and `scan`"); + } match cli.command { None => run_target( Operation::Check, @@ -436,17 +490,41 @@ pub fn run() -> Result { ..Default::default() }, &common, - false, + RunFlags::NONE, ), - Some(Command::Check(args)) => run_target(Operation::Check, args, &common, false), + Some(Command::Check(args)) => run_target(Operation::Check, args, &common, RunFlags::NONE), // `--dry-run` runs the diff and reports it in fix vocabulary: the two // commands must agree on the patch, so only the wording differs. Some(Command::Fix(args)) if args.dry_run => { - run_target(Operation::Diff, args.target(), &common, true) + run_target(Operation::Diff, args.target(), &common, RunFlags::DRY_RUN) + } + Some(Command::Fix(args)) if args.interactive => { + // The prompt is prose on a terminal and the answers come back the + // same way; a machine format has nowhere to put either, so the + // combination is refused rather than one of the two flags being + // quietly dropped. It is refused before the terminal is looked at, + // because the pair is wrong however the run was started. + if common.output.format != OutputFormat::Human { + bail!("--interactive is only available with --format human"); + } + // Without somebody there to answer, the questions would be read out + // of whatever the pipe happened to carry and files would be + // rewritten from it. Nothing is scanned, let alone written. + if !io::stdin().is_terminal() || !io::stdout().is_terminal() { + bail!("--interactive needs a terminal; run without -i or use `ocomment diff`"); + } + run_target( + Operation::Fix, + args.target(), + &common, + RunFlags::INTERACTIVE, + ) } - Some(Command::Fix(args)) => run_target(Operation::Fix, args.target(), &common, false), - Some(Command::Diff(args)) => run_target(Operation::Diff, args, &common, false), - Some(Command::Scan(args)) => run_target(Operation::Scan, args, &common, false), + Some(Command::Fix(args)) => { + run_target(Operation::Fix, args.target(), &common, RunFlags::NONE) + } + Some(Command::Diff(args)) => run_target(Operation::Diff, args, &common, RunFlags::NONE), + Some(Command::Scan(args)) => run_target(Operation::Scan, args, &common, RunFlags::NONE), Some(Command::Strip) => run_strip(&common), Some(Command::Lsp) => lsp::run(common.config.as_deref()), Some(Command::Init(args)) => run_init(args), @@ -463,10 +541,10 @@ fn run_target( operation: Operation, args: TargetArgs, common: &CommonArgs, - dry_run: bool, + flags: RunFlags, ) -> Result { let mut resolved = config::load(common.config.as_deref())?; - apply_cli_overrides(&mut resolved.config, common); + apply_cli_overrides(&mut resolved, common); let plugin_host = plugin::PluginHost::load(&resolved.root, &resolved.config.plugins)?; let presentation = presentation(common); let verbosity = common.verbosity(); @@ -477,11 +555,29 @@ fn run_target( } let progress = progress_enabled(common); let staged = args.git.staged || resolved.config.git.staged; + if operation == Operation::Fix && !staged && args.paths.is_empty() { + note_fix_scope(&resolved, common)?; + } + // `git` names a staged path relative to the repository root rather than to + // the working directory, so a staged run measures its paths against the + // root from there. Every other run measures them from where it was typed. + if staged && let Some(repository) = config::locate_repository(&resolved.cwd) { + resolved.cwd = repository; + } // `fix --dry-run` writes nothing, but it is still the command whose job is // to rewrite files in place, and standard input cannot be rewritten. - let rewrites = operation == Operation::Fix || dry_run; + let rewrites = operation == Operation::Fix || flags.dry_run; let (paths, stdin) = target_paths(&args.paths, rewrites, staged)?; if staged { + // A staged run reports index blobs through a path that carries no + // policy trace, so it says so rather than printing a listing with + // every explanation quietly missing. + if common.output.explain { + bail!( + "--explain is not available with --staged; explain the working tree with \ + `ocomment check --explain`" + ); + } return git::run_staged(git::StagedRequest { operation, paths: &paths, @@ -494,18 +590,28 @@ fn run_target( presentation, verbosity, preview: !common.output.no_preview, - dry_run, + dry_run: flags.dry_run, }); } let discovery = read_targets(&paths, stdin, &resolved, common)?; let total = discovery.files.len(); let counter = Progress::default(); + let explain = common.output.explain; let processed = discovery .files .par_iter() .map(|file| { - let (mut language, mut options) = - resolved.for_path(&file.path, file.language, file.dialect); + // Only an explaining run pays for the trace; every other one takes + // the hot path it always took. + let (mut language, mut options, trace) = if explain { + let (language, options, trace) = + resolved.for_path_traced(&file.path, file.language, file.dialect); + (language, options, Some(trace)) + } else { + let (language, options) = + resolved.for_path(&file.path, file.language, file.dialect); + (language, options, None) + }; if let Some(value) = common.language() { language = value; } @@ -513,6 +619,13 @@ fn run_target( config::validate_dialect(language, value)?; options.scan.dialect = value; } + // Recorded as the scan is about to run with them, `--language` and + // `--dialect` included, so an explanation accounts for the run that + // actually happened. + let material = trace.map(|trace| FileExplanation { + options: options.scan.clone(), + trace, + }); let result = if let Some(name) = &file.plugin { let language_name = file .path @@ -530,23 +643,44 @@ fn run_target( if progress { counter.report(total); } - Ok::<_, anyhow::Error>(ProcessedFile { - path: file.path.clone(), - source: file.source.clone(), - language, - result, - }) + Ok::<_, anyhow::Error>(( + ProcessedFile { + path: file.path.clone(), + source: file.source.clone(), + language, + result, + }, + material, + )) }) .collect::>>(); if progress { counter.clear(); } - let files = processed?; + // The explanations travel beside the files rather than inside them: a + // staged run reports the same `ProcessedFile` and has no trace to put in + // one, and the path is what the renderer looks each file up by anyway. + let processed = processed?; + let mut explanations = Explanations::new(); + let mut files = Vec::with_capacity(processed.len()); + for (file, material) in processed { + if let Some(material) = material { + explanations.insert(file.path.clone(), material); + } + files.push(file); + } let report_invalid = output::invalid(&files); let io_invalid = discovery.skipped.iter().any(|item| item.error); let invalid = report_invalid || io_invalid; let may_fix = !io_invalid && (!report_invalid || resolved.config.policy.force_invalid); + // An interactive run replaces the whole `fix` report: what it wrote is the + // answers it was given, and the ordinary summary counts what the run + // *could* have removed. A run the invalid-file gate has already stopped + // falls through instead, so that report says why nothing was written. + if flags.interactive && may_fix { + return run_interactive(&files, &discovery.skipped, invalid, presentation, verbosity); + } let applied = operation == Operation::Fix && may_fix; if applied { let plans = files @@ -560,7 +694,7 @@ fn run_target( .collect(); apply_transaction(plans)?; } - output::render( + output::render_explained( &files, &discovery.skipped, &RenderOptions { @@ -569,12 +703,13 @@ fn run_target( presentation, verbosity, preview: !common.output.no_preview, - explain: false, - dry_run, + explain, + dry_run: flags.dry_run, force_invalid: resolved.config.policy.force_invalid, applied, policy: resolved.config.policy.mode, }, + &explanations, )?; if invalid { return Ok(2); @@ -585,6 +720,58 @@ fn run_target( } } +/// Ask about each comment this run would remove, write the accepted removals +/// through the same transaction a plain `fix` uses, and report what the answers +/// came to. +/// +/// A clean abort is not a failure of the run: `x` is the answer for a fix that +/// should never have started, and it exits 0 having touched nothing. +fn run_interactive( + files: &[ProcessedFile], + skipped: &[files::SkippedFile], + invalid: bool, + presentation: Presentation, + verbosity: Verbosity, +) -> Result { + let offered: usize = files.iter().map(|file| file.result.edits.len()).sum(); + let selection = { + let stdin = io::stdin(); + let mut answers = stdin.lock(); + let mut questions = output::stdout(); + let selection = interactive::select(files, &mut answers, &mut questions, &presentation)?; + // The conversation is on standard output and the verdict that follows + // is on standard error; a terminal sees both, so the buffer is emptied + // first to keep them in the order they were written. + output::finish(&mut questions)?; + selection + }; + let outcome = output::InteractiveOutcome { + removed: selection.accepted, + reviewed: selection.accepted + selection.declined, + offered, + changed: selection.plans.len(), + scanned: files.len(), + }; + let aborted = selection.aborted; + if !aborted { + apply_transaction(selection.plans)?; + } + let stderr = io::stderr(); + let mut report = stderr.lock(); + if aborted { + output::note(&mut report, "Aborted; nothing was written.")?; + return Ok(0); + } + // A skipped path can be the whole answer to a run that was never asked a + // question, so the one command that writes no report of its own still says + // why it passed a file over. + for line in output::skip_lines(skipped, presentation, verbosity) { + output::note(&mut report, &line)?; + } + output::note(&mut report, &output::interactive_summary(outcome))?; + if invalid { Ok(2) } else { Ok(0) } +} + /// How the PATH list names standard input. const STDIN_ARGUMENT: &str = "-"; @@ -665,7 +852,7 @@ fn run_strip(common: &CommonArgs) -> Result { .read_to_end(&mut source) .context("cannot read standard input")?; let mut resolved = config::load(common.config.as_deref())?; - apply_cli_overrides(&mut resolved.config, common); + apply_cli_overrides(&mut resolved, common); let detection = common .language() .map(|language| (language, common.dialect().unwrap_or(Dialect::Standard))) @@ -704,21 +891,32 @@ fn run_strip(common: &CommonArgs) -> Result { Ok(if result.report.valid { 0 } else { 2 }) } -fn apply_cli_overrides(config: &mut config::Config, common: &CommonArgs) { +/// Layer the command line over the merged configuration, noting what it +/// overrode so `--explain` can name the flag rather than a file that never +/// mentioned the setting. +fn apply_cli_overrides(resolved: &mut config::ResolvedConfig, common: &CommonArgs) { let policy = &common.policy; + let config = &mut resolved.config; + let overrides = &mut resolved.cli_overrides; if let Some(value) = policy.policy { config.policy.mode = *value; + overrides.policy = true; } if let Some(value) = policy.layout { config.policy.layout = *value; + overrides.layout = true; } if !policy.keep_kind.is_empty() { + // The flag adds to the configured list rather than replacing it, so + // the boundary is what tells the two apart afterwards. + overrides.keep_kind_from = Some(config.policy.keep_kind.len()); config .policy .keep_kind .extend(policy.keep_kind.iter().copied().map(CommentKind::from)); } if !policy.remove_kind.is_empty() { + overrides.remove_kind_from = Some(config.policy.remove_kind.len()); config .policy .remove_kind @@ -976,7 +1174,7 @@ fn run_config(args: ConfigArgs, common: &CommonArgs) -> Result { } action => { let mut resolved = config::load(common.config.as_deref())?; - apply_cli_overrides(&mut resolved.config, common); + apply_cli_overrides(&mut resolved, common); match action { ConfigAction::Show => { resolved.config.version = Some(1); @@ -1008,7 +1206,7 @@ fn run_config(args: ConfigArgs, common: &CommonArgs) -> Result { stdout, "precedence: built-in < XDG user < project < path override < CLI" ))?; - output::wrote(writeln!(stdout, "root: {}", resolved.root.display()))?; + output::wrote(writeln!(stdout, "root: {}", root_row(&resolved)))?; output::wrote(writeln!( stdout, "policy: {}; layout: {}", @@ -1163,11 +1361,15 @@ fn run_doctor(common: &CommonArgs) -> Result { let mut stdout = output::stdout(); output::wrote(writeln!(stdout, "ocomment {}", env!("CARGO_PKG_VERSION")))?; match std::env::current_dir() { - Ok(directory) => output::wrote(writeln!(stdout, "cwd: {}", directory.display()))?, + Ok(directory) => output::wrote(writeln!( + stdout, + "cwd: {}", + output::sanitize_path(&directory.to_string_lossy()) + ))?, Err(error) => output::wrote(writeln!(stdout, "cwd: unavailable ({error})"))?, } let resolved = config::load(common.config.as_deref())?; - output::wrote(writeln!(stdout, "root: {}", resolved.root.display()))?; + output::wrote(writeln!(stdout, "root: {}", root_row(&resolved)))?; for source in config_trace(&resolved.trace) { output::wrote(writeln!(stdout, "config: {source}"))?; } @@ -1285,22 +1487,65 @@ fn presentation(common: &CommonArgs) -> Presentation { } } +/// The project root, as a report names it. +/// +/// A directory name is chosen by whoever made the directory, not by OComment, +/// so a row carrying one is untrusted text on its way to a terminal for the +/// same reason a probed tool's version line is — and, unlike one, it must not +/// be cut short: a path that ends in an ellipsis names no directory at all. +fn root_row(resolved: &config::ResolvedConfig) -> String { + output::sanitize_path(&resolved.root.to_string_lossy()) +} + +/// What the run was pointed at, in the words the caller used, or the implicit +/// target that stands in when they named nothing. +fn target_label(paths: &[PathBuf]) -> String { + if paths.is_empty() { + return files::DEFAULT_TARGET.to_owned(); + } + paths + .iter() + .map(|path| path.display().to_string()) + .collect::>() + .join(" ") +} + +/// Say where a bare `fix` is pointed when that is not where the project +/// starts. +/// +/// A reader who has only ever run `ocomment fix` from the top of a repository +/// can read the bare command as "fix the project", and it is the one command +/// that writes. So the run that was told nothing about where to write names +/// both the target it chose and the root the configuration came from, once, +/// before it starts. A caller who named a path has already said what they +/// meant, and from the root itself the two are the same directory: either way +/// the line would be noise. +fn note_fix_scope(resolved: &config::ResolvedConfig, common: &CommonArgs) -> Result<()> { + if resolved.cwd == resolved.root + || common.output.format != OutputFormat::Human + || common.verbosity() == Verbosity::Quiet + { + return Ok(()); + } + let stderr = io::stderr(); + let mut report = stderr.lock(); + output::note( + &mut report, + &format!( + "note: fixing files under {} (project root: {})", + files::DEFAULT_TARGET, + root_row(resolved) + ), + ) +} + /// The `--verbose` header: where the run is rooted, what it was pointed at, /// and which configuration files it merged. fn trace_run(resolved: &config::ResolvedConfig, paths: &[PathBuf]) -> Result<()> { let stderr = io::stderr(); let mut report = stderr.lock(); - output::note(&mut report, &format!("root: {}", resolved.root.display()))?; - let target = if paths.is_empty() { - ".".to_owned() - } else { - paths - .iter() - .map(|path| path.display().to_string()) - .collect::>() - .join(" ") - }; - output::note(&mut report, &format!("target: {target}"))?; + output::note(&mut report, &format!("root: {}", root_row(resolved)))?; + output::note(&mut report, &format!("target: {}", target_label(paths)))?; for source in config_trace(&resolved.trace) { output::note(&mut report, &format!("config: {source}"))?; } @@ -1320,8 +1565,10 @@ fn config_trace(trace: &config::ConfigTrace) -> Vec { ] .into_iter() .filter_map(|(label, path)| { + // The row carries a directory name OComment did not choose, so it is + // sanitised for the same reason a `root` row is. path.as_ref() - .map(|path| format!("{label} {}", path.display())) + .map(|path| format!("{label} {}", output::sanitize_path(&path.to_string_lossy()))) }) .collect(); if sources.is_empty() { diff --git a/rust/ocomment/src/config.rs b/rust/ocomment/src/config.rs index 772c5d6..37a6327 100644 --- a/rust/ocomment/src/config.rs +++ b/rust/ocomment/src/config.rs @@ -1,14 +1,14 @@ use anyhow::{Context, Result, anyhow, bail, ensure}; use globset::{Glob, GlobMatcher}; use ocomment_core::{ - CommentKind, DeclarativeProfile, Dialect, Language, Layout, Policy, ScanOptions, - TransformOptions, validate_profile, + CommentKind, DeclarativeProfile, Dialect, DispositionExplanation, Language, Layout, Policy, + ScanOptions, TransformOptions, validate_profile, }; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeMap, env, fs, - path::{Path, PathBuf}, + path::{Component, Path, PathBuf}, }; pub const CONFIG_FILE: &str = ".ocomment.toml"; @@ -142,6 +142,198 @@ pub struct PathOverride { pub remove_regex: Vec, } +/// Where one effective setting came from. +/// +/// The layers are the ones [`ResolvedConfig::for_path`] merges, and a source +/// names the layer a value arrived on rather than the value itself, so +/// `--explain` can send a reader to the table they have to edit. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub enum Source { + /// The `[policy]` table, or the built-in default when no file set it. + #[default] + Global, + /// A `[languages.]` table. + Language(String), + /// The `[[overrides]]` table at `index`, whose globs matched the path. + Override { index: usize, paths: Vec }, + /// A flag on the command line. + Cli { flag: &'static str }, +} + +impl Source { + /// How an explanation names this source, given the file a `Global` value + /// was written in. + /// + /// `#N` counts an `[[overrides]]` table from zero, the way the regex + /// indices printed beside it count the patterns they address. + fn describe(&self, origin: Option<&Path>) -> String { + match self { + Self::Global => match origin { + Some(path) => format!("[policy] in {}", path.display()), + None => "built-in defaults".to_owned(), + }, + Self::Language(name) => format!("[languages.{name}]"), + Self::Override { index, paths } => { + format!("[[overrides]] #{index}, paths = {paths:?}") + } + Self::Cli { flag } => format!("{flag} on the command line"), + } + } +} + +/// The `[policy]` keys a trace can attribute to a file, spelled as the file +/// spells them. +const POLICY_KEYS: [&str; 6] = [ + "mode", + "layout", + "keep_kind", + "remove_kind", + "keep_regex", + "remove_regex", +]; + +/// Which configuration file last set each `[policy]` key. A key no file sets +/// keeps no entry, and an explanation calls it a built-in default rather than +/// sending the reader to a file that never mentions it. +type PolicyOrigins = BTreeMap<&'static str, PathBuf>; + +/// What the command line overrode, recorded while it was applied so a trace +/// can say the command line rather than the file the value would otherwise +/// have been written in. +#[derive(Clone, Copy, Debug, Default)] +pub struct CliOverrides { + pub policy: bool, + pub layout: bool, + /// Where the `--keep-kind` values start in `policy.keep_kind`: the command + /// line appends to the configured list instead of replacing it, so only + /// the tail of that list belongs to the command line. + pub keep_kind_from: Option, + /// The same boundary for `--remove-kind` in `policy.remove_kind`. + pub remove_kind_from: Option, +} + +/// Where every effective setting for one path came from. +/// +/// The `*_kind` and `*_regex` vectors run parallel to the vectors in the +/// [`ScanOptions`] that [`ResolvedConfig::for_path_traced`] returned beside +/// this: entry `i` says which layer contributed entry `i` of that list. +#[derive(Clone, Debug, Default)] +pub struct PolicyTrace { + pub policy: Source, + /// Where the layout came from. No disposition depends on the layout, so no + /// explanation names it yet; it is recorded because the trace answers the + /// question for the whole `[policy]` block and `--explain` will not be its + /// only caller. + #[allow(dead_code)] + pub layout: Source, + pub keep_kind: Vec, + pub remove_kind: Vec, + pub keep_regex: Vec, + pub remove_regex: Vec, + origins: PolicyOrigins, +} + +impl PolicyTrace { + /// Where the setting that decided `explanation` came from, worded the way + /// `--explain` prints it, or `None` when a built-in rule decided it and + /// there is no table to point at. + /// + /// `options` is the one the explanation was produced from: a regex + /// explanation carries its index into those lists, and a kind explanation + /// is found by the kind it names. + pub fn origin_of( + &self, + explanation: &DispositionExplanation, + options: &ScanOptions, + ) -> Option { + let position = |kinds: &[CommentKind], kind: &CommentKind| { + kinds.iter().position(|value| value == kind) + }; + let (source, key) = match explanation { + DispositionExplanation::KeptByKind(kind) => ( + self.keep_kind.get(position(&options.keep_kinds, kind)?)?, + "keep_kind", + ), + DispositionExplanation::RemovedByKind(kind) => ( + self.remove_kind + .get(position(&options.remove_kinds, kind)?)?, + "remove_kind", + ), + DispositionExplanation::KeptByRegex { index, .. } => { + (self.keep_regex.get(*index)?, "keep_regex") + } + DispositionExplanation::RemovedByRegex { index, .. } => { + (self.remove_regex.get(*index)?, "remove_regex") + } + // Every one of these is the policy having the last word, whether it + // took the comment out or protected it. + DispositionExplanation::RemovedByPolicy(_) + | DispositionExplanation::RemovedByDefault(_) + | DispositionExplanation::KeptLicense { .. } => (&self.policy, "mode"), + // A built-in rule, decided by no setting at all. + DispositionExplanation::ProtectedPreamble + | DispositionExplanation::KeptHtml + | DispositionExplanation::KeptDirective { .. } => return None, + }; + Some(source.describe(self.origins.get(key).map(PathBuf::as_path))) + } +} + +/// One layer of the policy merge, as the trace replays it. +struct TracedLayer<'a> { + source: Source, + policy: Option, + layout: Option, + keep_kind: &'a [CommentKind], + remove_kind: &'a [CommentKind], + keep_regex: &'a [String], + remove_regex: &'a [String], +} + +/// Attribute every entry of one merged list to the layer that introduced it. +/// +/// [`ResolvedConfig::for_path`] starts from the global list verbatim and then +/// appends whatever a later layer adds that is not there yet, so replaying that +/// walk reproduces the merged list position for position. The tail of the +/// global list from `cli_from` on is what `flag` appended to it. +fn attribute( + global: &[T], + cli_from: Option, + flag: &'static str, + layers: &[(&Source, &[T])], +) -> Vec { + let cli_from = cli_from.unwrap_or(usize::MAX); + let mut sources: Vec = (0..global.len()) + .map(|index| { + if index >= cli_from { + Source::Cli { flag } + } else { + Source::Global + } + }) + .collect(); + let mut seen = global.to_vec(); + for (source, values) in layers { + for value in *values { + if !seen.contains(value) { + seen.push(value.clone()); + sources.push((*source).clone()); + } + } + } + sources +} + +/// Where a setting that holds a single value came from, before any language or +/// path layer has had its say. +fn scalar_source(overridden: bool, flag: &'static str) -> Source { + if overridden { + Source::Cli { flag } + } else { + Source::Global + } +} + struct CompiledOverride { matchers: Vec, value: PathOverride, @@ -157,19 +349,55 @@ pub struct ConfigTrace { pub struct ResolvedConfig { pub config: Config, pub trace: ConfigTrace, + /// Where the project starts: the directory `.ocomment.toml` was found in, + /// the repository above the working directory, or the working directory + /// itself. It decides where configuration is discovered, what the file and + /// override globs are written relative to, and where the plugin lock + /// lives — no longer what a command with no path walks. pub root: PathBuf, + /// The directory the command was run from, which is what a path typed on + /// the command line is relative to. + pub cwd: PathBuf, + /// What the command line overrode, filled in after the files were merged. + pub cli_overrides: CliOverrides, overrides: Vec, + origins: PolicyOrigins, } impl ResolvedConfig { + /// Where `path` sits under the project root, spelled the way a + /// configuration glob is written. + /// + /// `files.include`, `files.exclude`, and every `[[overrides]].paths` + /// pattern is relative to the root, while a path named on the command line + /// is relative to the working directory. The two agree only when the + /// command is run from the root, so the path is resolved against the + /// directory it was typed in before it is measured against the root, and + /// the separators come out as forward slashes so one glob reads the same + /// on every platform. + /// + /// A path outside the root — an explicit target above it, say — has no + /// root-relative spelling at all, so it keeps its absolute one and only an + /// absolute glob can match it. + pub fn relative_to_root(&self, path: &Path) -> String { + // Standard input has no place on disk. The pseudo-path is what the + // renderers print, so it is also what the globs are shown. + if path.as_os_str() == crate::files::STDIN_PATH { + return crate::files::STDIN_PATH.to_owned(); + } + let joined = self.cwd.join(path); + let absolute = lexical(&std::path::absolute(&joined).unwrap_or(joined)); + let relative = absolute.strip_prefix(&self.root).unwrap_or(&absolute); + relative.to_string_lossy().replace('\\', "/") + } + pub fn for_path( &self, path: &Path, language: Language, dialect: Dialect, ) -> (Language, TransformOptions) { - let relative = path.strip_prefix(&self.root).unwrap_or(path); - let normalized = relative.to_string_lossy().replace('\\', "/"); + let normalized = self.relative_to_root(path); let mut chosen_language = language; let mut chosen_dialect = dialect; let mut policy = self.config.policy.mode; @@ -230,6 +458,109 @@ impl ResolvedConfig { }; (chosen_language, TransformOptions { scan, layout }) } + + /// The same answer as [`Self::for_path`], with a record of where each + /// setting came from. + /// + /// The values are [`Self::for_path`]'s own, so what a run does and what + /// `--explain` says about it cannot disagree; only the attribution is + /// computed here, by replaying the same merge with the layer names + /// attached. `--explain` is the only caller, which is why the hot path is + /// left as it was. + pub fn for_path_traced( + &self, + path: &Path, + language: Language, + dialect: Dialect, + ) -> (Language, TransformOptions, PolicyTrace) { + let (chosen_language, options) = self.for_path(path, language, dialect); + let normalized = self.relative_to_root(path); + let mut layers = Vec::new(); + // `for_path` looks the language table up under the language it was + // handed, not under the one an override may have changed it to. + if let Some(config) = self.config.languages.get(language.as_str()) { + layers.push(TracedLayer { + source: Source::Language(language.as_str().to_owned()), + policy: config.policy, + layout: config.layout, + keep_kind: &config.keep_kind, + remove_kind: &config.remove_kind, + keep_regex: &config.keep_regex, + remove_regex: &config.remove_regex, + }); + } + for (index, compiled) in self.overrides.iter().enumerate() { + if !compiled + .matchers + .iter() + .any(|matcher| matcher.is_match(&normalized)) + { + continue; + } + let value = &compiled.value; + layers.push(TracedLayer { + source: Source::Override { + index, + paths: value.paths.clone(), + }, + policy: value.policy, + layout: value.layout, + keep_kind: &value.keep_kind, + remove_kind: &value.remove_kind, + keep_regex: &value.keep_regex, + remove_regex: &value.remove_regex, + }); + } + let cli = self.cli_overrides; + let keep_kinds: Vec<_> = layers + .iter() + .map(|layer| (&layer.source, layer.keep_kind)) + .collect(); + let remove_kinds: Vec<_> = layers + .iter() + .map(|layer| (&layer.source, layer.remove_kind)) + .collect(); + let keep_patterns: Vec<_> = layers + .iter() + .map(|layer| (&layer.source, layer.keep_regex)) + .collect(); + let remove_patterns: Vec<_> = layers + .iter() + .map(|layer| (&layer.source, layer.remove_regex)) + .collect(); + let mut trace = PolicyTrace { + policy: scalar_source(cli.policy, "--policy"), + layout: scalar_source(cli.layout, "--layout"), + keep_kind: attribute( + &self.config.policy.keep_kind, + cli.keep_kind_from, + "--keep-kind", + &keep_kinds, + ), + remove_kind: attribute( + &self.config.policy.remove_kind, + cli.remove_kind_from, + "--remove-kind", + &remove_kinds, + ), + // No flag supplies a pattern, so no entry of either list can have + // come from the command line. + keep_regex: attribute(&self.config.policy.keep_regex, None, "", &keep_patterns), + remove_regex: attribute(&self.config.policy.remove_regex, None, "", &remove_patterns), + origins: self.origins.clone(), + }; + // A single-valued setting is not merged but replaced, so the last layer + // that names it is the one that decided it. + for layer in &layers { + if layer.policy.is_some() { + trace.policy = layer.source.clone(); + } + if layer.layout.is_some() { + trace.layout = layer.source.clone(); + } + } + (chosen_language, options, trace) + } } pub fn load(explicit: Option<&Path>) -> Result { @@ -244,16 +575,35 @@ pub fn load(explicit: Option<&Path>) -> Result { let user_path = user_config_path().filter(|path| path.is_file()); let mut merged: toml::Value = toml::from_str(&toml::to_string(&Config::default())?)?; let mut trace = ConfigTrace::default(); + let mut origins = PolicyOrigins::new(); if let Some(path) = &user_path { - merge_value(&mut merged, parse_layer(path, false)?); + merge_layer( + &mut merged, + &mut origins, + parse_layer(path, false)?, + path, + &cwd, + ); trace.user = Some(path.clone()); } if let Some(path) = &project_path { - merge_value(&mut merged, parse_layer(path, true)?); + merge_layer( + &mut merged, + &mut origins, + parse_layer(path, true)?, + path, + &cwd, + ); trace.project = Some(path.clone()); } if let Some(path) = explicit { - merge_value(&mut merged, parse_layer(path, true)?); + merge_layer( + &mut merged, + &mut origins, + parse_layer(path, true)?, + path, + &cwd, + ); trace.explicit = Some(path.to_path_buf()); } let mut config: Config = merged @@ -272,10 +622,47 @@ pub fn load(explicit: Option<&Path>) -> Result { config, trace, root, + cwd, + cli_overrides: CliOverrides::default(), overrides, + origins, }) } +/// Layer one configuration file over the merged document, noting every +/// `[policy]` key it sets on the way. +/// +/// A later layer overwrites an earlier one exactly as `merge_value` does, so +/// what is left is the file whose value survived the merge — the one an +/// explanation is worth sending a reader to. +fn merge_layer( + merged: &mut toml::Value, + origins: &mut PolicyOrigins, + layer: toml::Value, + path: &Path, + cwd: &Path, +) { + if let Some(policy) = layer.get("policy") { + for key in POLICY_KEYS { + if policy.get(key).is_some() { + origins.insert(key, origin_label(path, cwd)); + } + } + } + merge_value(merged, layer); +} + +/// How an explanation names a configuration file: relative to the directory +/// the command was run from when it sits there, and absolute otherwise. +/// +/// The label is repeated on every explained line, so the short spelling is +/// worth having — but only where it still names the file the reader would open. +/// A file further up the tree, or the user file under `$HOME`, keeps its +/// absolute path. +fn origin_label(path: &Path, cwd: &Path) -> PathBuf { + path.strip_prefix(cwd).unwrap_or(path).to_path_buf() +} + fn validate_languages(config: &Config) -> Result<()> { for (name, language_config) in &config.languages { let language: Language = name.parse().map_err(|_| { @@ -436,6 +823,34 @@ fn compile_overrides(overrides: &[PathOverride]) -> Result .collect() } +/// Resolve `.` and `..` without asking the file system. +/// +/// A configuration glob is matched against text, so the text has to be the one +/// the reader would have written: `../sibling/main.rs`, named from `nested/`, +/// is `sibling/main.rs` under the root, and leaving the `..` in place would +/// let it match a `nested/**` override it is not under. The resolution is +/// lexical because the path need not exist and because `canonicalize` would +/// also resolve the symbolic links the root itself may be reached through, +/// which would leave the two ends of the comparison in different spellings. +fn lexical(path: &Path) -> PathBuf { + let mut resolved = PathBuf::new(); + for component in path.components() { + match component { + Component::CurDir => {} + Component::ParentDir + if matches!( + resolved.components().next_back(), + Some(Component::Normal(_)) + ) => + { + resolved.pop(); + } + component => resolved.push(component), + } + } + resolved +} + pub fn locate_project(start: &Path) -> Option { let mut directory = Some(start); while let Some(current) = directory { @@ -531,6 +946,74 @@ mod tests { assert!(unknown_key_hint(message).contains("layout")); } + /// `for_path_traced` must not become a second copy of the merge that can + /// drift from it: the values it returns are `for_path`'s own, and the trace + /// beside them lines up with those values position for position. + #[test] + fn a_traced_lookup_returns_the_untraced_answer_and_lines_up_with_it() { + let directory = tempfile::tempdir().unwrap(); + let root = directory.path().to_path_buf(); + let mut config = Config::default(); + config.policy.keep_regex = vec!["global".to_owned()]; + config.policy.keep_kind = vec![CommentKind::Line]; + config.languages.insert( + "rust".to_owned(), + LanguageConfig { + keep_regex: vec!["language".to_owned()], + ..LanguageConfig::default() + }, + ); + config.overrides = vec![PathOverride { + paths: vec!["nested/**".to_owned()], + policy: Some(Policy::All), + // The duplicate is dropped by the merge, so the trace must not + // record a source for it either. + keep_regex: vec!["override".to_owned(), "global".to_owned()], + ..PathOverride::default() + }]; + let overrides = compile_overrides(&config.overrides).unwrap(); + let resolved = ResolvedConfig { + config, + trace: ConfigTrace::default(), + root: root.clone(), + cwd: root.clone(), + cli_overrides: CliOverrides { + keep_kind_from: Some(0), + ..CliOverrides::default() + }, + overrides, + origins: PolicyOrigins::new(), + }; + let path = root.join("nested/a.rs"); + + let (language, options) = resolved.for_path(&path, Language::Rust, Dialect::Standard); + let (traced_language, traced_options, trace) = + resolved.for_path_traced(&path, Language::Rust, Dialect::Standard); + assert_eq!(traced_language, language); + assert_eq!(traced_options, options); + + let override_source = Source::Override { + index: 0, + paths: vec!["nested/**".to_owned()], + }; + assert_eq!(options.scan.keep_regex, ["global", "language", "override"]); + assert_eq!( + trace.keep_regex, + [ + Source::Global, + Source::Language("rust".to_owned()), + override_source.clone(), + ] + ); + assert_eq!(trace.policy, override_source); + assert_eq!( + trace.keep_kind, + [Source::Cli { + flag: "--keep-kind" + }] + ); + } + #[test] fn repository_root_accepts_directory_and_worktree_markers() { for marker_is_directory in [true, false] { diff --git a/rust/ocomment/src/files.rs b/rust/ocomment/src/files.rs index 1a9a303..83d3439 100644 --- a/rust/ocomment/src/files.rs +++ b/rust/ocomment/src/files.rs @@ -127,13 +127,31 @@ pub fn stdin_source( }) } +/// What a command with no PATH walks. +/// +/// The project root is where the configuration was found, not what the caller +/// is looking at: a command run from a subdirectory checks that subdirectory, +/// the way every other file-walking developer tool does. Reaching back up to +/// the root would put files the caller cannot see — and, with `fix`, files +/// they did not mean to rewrite — into the run. +pub const DEFAULT_TARGET: &str = "."; + pub fn discover( paths: &[PathBuf], resolved: &ResolvedConfig, forced_language: Option, forced_dialect: Option, ) -> Result { - discover_with_scope(paths, resolved, forced_language, forced_dialect, true) + let implicit = [PathBuf::from(DEFAULT_TARGET)]; + // The substituted target stands in for an argument nobody typed, so it is + // walked with the ordinary limits: only a path the caller actually named + // is a request to look past the hidden-file and size rules. + let (paths, explicit) = if paths.is_empty() { + (&implicit[..], false) + } else { + (paths, true) + }; + discover_with_scope(paths, resolved, forced_language, forced_dialect, explicit) } /// Discover workspace roots with normal traversal limits. Unlike explicit CLI @@ -152,6 +170,8 @@ fn discover_with_scope( let include = compile_globs(&resolved.config.files.include)?; let exclude = compile_globs(&resolved.config.files.exclude)?; let mut discovery = Discovery::default(); + // Only an editor asking for its workspace arrives here without a target; + // `discover` gives a command line the current directory instead. let targets: Vec<_> = if paths.is_empty() { vec![(resolved.root.clone(), false)] } else { @@ -240,6 +260,20 @@ fn discover_with_scope( Ok(discovery) } +/// The name a walked file is reported under. +/// +/// The implicit target is `.`, so a walk rooted there hands back every entry +/// as `./name`. `ocomment` and `ocomment check name` report one file, and a +/// reader — or a `git apply` reading the patch — is owed one spelling of it, +/// so the prefix the walk root contributed is dropped. The target itself is +/// left alone: `.` names a directory, and `` names nothing. +fn reported_path(path: &Path) -> PathBuf { + match path.strip_prefix(DEFAULT_TARGET) { + Ok(stripped) if !stripped.as_os_str().is_empty() => stripped.to_path_buf(), + _ => path.to_path_buf(), + } +} + #[allow(clippy::too_many_arguments)] fn load_one( path: &Path, @@ -252,8 +286,12 @@ fn load_one( exclude: &GlobSet, discovery: &mut Discovery, ) { - let relative = path.strip_prefix(&resolved.root).unwrap_or(path); - if (!include.is_empty() && !include.is_match(relative)) || exclude.is_match(relative) { + let path = &reported_path(path); + // The globs are written relative to the root; the path was typed — or + // walked — relative to the working directory, so it is measured against + // the root before either set is asked about it. + let relative = resolved.relative_to_root(path); + if (!include.is_empty() && !include.is_match(&relative)) || exclude.is_match(&relative) { return; } let link_metadata = match path.symlink_metadata() { diff --git a/rust/ocomment/src/interactive.rs b/rust/ocomment/src/interactive.rs new file mode 100644 index 0000000..a80e59c --- /dev/null +++ b/rust/ocomment/src/interactive.rs @@ -0,0 +1,758 @@ +//! The comment-by-comment prompt behind `fix --interactive`. +//! +//! The run has already transformed every file by the time this module is +//! reached, so what it asks about is a list of edits that were computed +//! together. Applying only some of them is safe because a replacement is +//! computed from the *source* alone: under `layout = "columns"` it is exactly +//! as wide as the comment it stands for, so a removal moves nothing that comes +//! after it, and under every other layout it depends only on the bytes either +//! side of its own span. `partial_column_edits_keep_the_replacement_the_transform_computed` +//! pins that. +//! +//! The prompt is line-based on purpose: no raw mode, no cursor addressing, no +//! terminal library. One question, one line of answer, and a transcript that a +//! test can read. + +use crate::{ + atomic::WritePlan, + output::{ + Presentation, ProcessedFile, color, line_column, sanitize_path, sanitize_source_line, wrote, + }, +}; +use anyhow::Result; +use ocomment_core::{Comment, Edit, apply_edits}; +use std::io::{BufRead, Write}; + +/// What the reader decided about the removable comments of one run. +/// +/// Deliberately not `Debug`: a plan carries the whole before-and-after text of +/// a source file, and the one thing this type must never do is put it on a +/// terminal by accident. +#[derive(Default)] +pub struct Selection { + /// One plan per file that keeps at least one accepted removal. + pub plans: Vec, + pub accepted: usize, + pub declined: usize, + /// The reader asked for the run to write nothing at all. + pub aborted: bool, +} + +/// The question, ending in a space rather than a newline so the answer is typed +/// on the same line. +const PROMPT: &str = "Remove? [y,n,a,d,q,x,?] "; + +/// What each answer does, in the order the prompt lists them. +const HELP: [&str; 7] = [ + "y - remove this comment", + "n - keep it", + "a - remove it and every remaining comment in this file", + "d - keep it and every remaining comment in this file", + "q - stop asking and apply the removals accepted so far", + "x - abort; write nothing", + "? - show this help", +]; + +/// What is said to an answer that is not one of them. A typo is never taken for +/// a decision about somebody's source file. +const UNKNOWN: &str = "unknown answer; press ? for help"; + +/// How many unchanged lines are shown either side of the change. +const CONTEXT: usize = 3; + +/// Ask about every comment this run would remove and collect the answers into +/// the writes they come to. +/// +/// `input` and `output` are the reader's terminal; they are parameters so the +/// whole conversation can be driven from a script in a test. +pub fn select( + files: &[ProcessedFile], + input: &mut dyn BufRead, + output: &mut dyn Write, + presentation: &Presentation, +) -> Result { + let total: usize = files.iter().map(|file| file.result.edits.len()).sum(); + let mut selection = Selection::default(); + let mut position = 0usize; + let mut stopped = false; + for file in files { + let items = offers(file); + if items.is_empty() { + continue; + } + let mut accepted: Vec = Vec::new(); + // The answer `a` or `d` left standing for the rest of this file. + let mut standing: Option = None; + for (index, item) in items.iter().enumerate() { + let (comment, edit) = *item; + position += 1; + let remove = match standing { + Some(answer) => answer, + None => { + let place = Place { + index: index + 1, + of: items.len(), + position, + total, + }; + show(output, file, comment, edit, place, presentation)?; + match ask(input, output, presentation)? { + Answer::Yes => true, + Answer::No => false, + Answer::AllInFile => { + standing = Some(true); + true + } + Answer::NoneInFile => { + standing = Some(false); + false + } + Answer::Stop => { + stopped = true; + break; + } + // Everything accepted so far goes with it: `x` is the + // answer for a run that should never have started. + Answer::Abort => { + return Ok(Selection { + aborted: true, + ..Selection::default() + }); + } + Answer::Help => unreachable!("`ask` answers `?` itself"), + } + } + }; + if remove { + selection.accepted += 1; + accepted.push(edit.clone()); + } else { + selection.declined += 1; + } + } + if !accepted.is_empty() { + let replacement = apply_edits(&file.source, &accepted); + if replacement != file.source { + selection.plans.push(WritePlan { + path: file.path.clone(), + original: file.source.clone(), + replacement, + }); + } + } + if stopped { + break; + } + } + Ok(selection) +} + +/// The comments this run would remove, each with the edit that removes it. +/// +/// `transform` pushes exactly one edit per removable comment, in source order, +/// so the two lists line up pairwise. A file whose source failed to scan has no +/// edits at all, and nothing about it is offered — the same gate a +/// non-interactive `fix` applies before it writes. +fn offers(file: &ProcessedFile) -> Vec<(&Comment, &Edit)> { + file.result + .report + .comments + .iter() + .filter(|comment| comment.disposition.is_remove()) + .zip(file.result.edits.iter()) + .collect() +} + +/// Where one question sits, in its file and in the run. +struct Place { + index: usize, + of: usize, + position: usize, + total: usize, +} + +/// Write the question's heading and the hunk it is about. +fn show( + output: &mut dyn Write, + file: &ProcessedFile, + comment: &Comment, + edit: &Edit, + place: Place, + presentation: &Presentation, +) -> Result<()> { + let (line, column) = line_column(&file.source, comment.span.start); + wrote(writeln!( + output, + "{}:{line}:{column} {} comment ({} of {} in file, {} of {} total)", + sanitize_path(&file.path.display().to_string()), + comment.kind, + place.index, + place.of, + place.position, + place.total + ))?; + for row in hunk(&file.source, edit, presentation) { + wrote(writeln!(output, "{row}"))?; + } + Ok(()) +} + +/// The lines the reader is answering for: the ones the comment sits on as they +/// are, the same ones as this single edit would leave them, and `CONTEXT` lines +/// of unchanged source either side. +/// +/// The "after" text is produced by applying this one edit and nothing else, so +/// what is shown is what answering `y` to this question alone would do. +fn hunk(source: &[u8], edit: &Edit, presentation: &Presentation) -> Vec { + let length = source.len(); + let begin = edit.span.start.min(length); + let finish = edit.span.end.clamp(begin, length); + let start = line_start(source, begin); + // The last byte the span covers, so a span that ends exactly on a line + // break does not drag the following line into the hunk. + let inner = if finish > begin { finish - 1 } else { begin }; + let end = line_end(source, inner); + let after = apply_edits(source, std::slice::from_ref(edit)); + let shift = edit.replacement.len() as isize - (finish - begin) as isize; + let moved = (end as isize + shift).clamp(start as isize, after.len() as isize); + #[expect( + clippy::cast_sign_loss, + reason = "clamped to `start..=after.len()`, both of which are lengths" + )] + let moved = moved as usize; + + let mut rows = Vec::new(); + for line in preceding(source, start, CONTEXT) { + rows.push(rendered(' ', line, presentation)); + } + changed(&mut rows, '-', &rows_of(&source[start..end]), presentation); + changed( + &mut rows, + '+', + &collapse_blanks(rows_of(&after[start..moved])), + presentation, + ); + for line in following(source, end, CONTEXT) { + rows.push(rendered(' ', line, presentation)); + } + rows +} + +/// How many lines of one changed side are shown before the rest are folded +/// into a single marker: `CONTEXT` at each end, the same window the unchanged +/// context gets. +const BLOCK: usize = 2 * CONTEXT; + +/// One side of the change, capped so a comment taller than the screen cannot +/// push the question off it. +/// +/// A block comment can run to any length, and the reader is answering about +/// the comment, not reading it here: the first and last `CONTEXT` lines say +/// which comment it is and where it ends, and the marker between them says how +/// much was left out rather than pretending there was nothing. +fn changed(rows: &mut Vec, marker: char, lines: &[&[u8]], presentation: &Presentation) { + let show = |rows: &mut Vec, block: &[&[u8]]| { + rows.extend( + block + .iter() + .map(|line| rendered(marker, line, presentation)), + ); + }; + if lines.len() <= BLOCK { + show(rows, lines); + return; + } + show(rows, &lines[..CONTEXT]); + rows.push(elision(marker, lines.len() - BLOCK, presentation)); + show(rows, &lines[lines.len() - CONTEXT..]); +} + +/// What stands in for the lines a capped side folded away. It carries the +/// marker of the side it belongs to so the two columns stay aligned, and is +/// dimmed rather than tinted so it is never read as a line of the source. +fn elision(marker: char, hidden: usize, presentation: &Presentation) -> String { + format!( + "{}{marker}... {hidden} more line{} ...{}", + color("\x1b[2m", presentation.color), + if hidden == 1 { "" } else { "s" }, + color("\x1b[0m", presentation.color) + ) +} + +/// Runs of the same blank line folded to one. +/// +/// Under `layout = "lines"` a removed block comment leaves exactly as many +/// empty lines as it occupied, and the twenty-seventh of them tells the reader +/// nothing the first did not. +fn collapse_blanks(lines: Vec<&[u8]>) -> Vec<&[u8]> { + let mut kept: Vec<&[u8]> = Vec::with_capacity(lines.len()); + for line in lines { + let blank = line.iter().all(u8::is_ascii_whitespace); + if blank && kept.last() == Some(&line) { + continue; + } + kept.push(line); + } + kept +} + +/// One line of the hunk: its marker, its terminal-safe text, and the colour +/// that says which of the three it is. +fn rendered(marker: char, line: &[u8], presentation: &Presentation) -> String { + let tint = match marker { + '-' => "\x1b[31m", + '+' => "\x1b[32m", + _ => "\x1b[2m", + }; + format!( + "{}{marker}{}{}", + color(tint, presentation.color), + sanitize_source_line(&String::from_utf8_lossy(line)), + color("\x1b[0m", presentation.color) + ) +} + +/// One block of bytes as the lines it holds, with the carriage return of a +/// CRLF file left out of the text rather than shown as a control character. +fn rows_of(block: &[u8]) -> Vec<&[u8]> { + block + .split(|byte| *byte == b'\n') + .map(|line| line.strip_suffix(b"\r").unwrap_or(line)) + .collect() +} + +/// The start of the line byte `offset` falls on. +fn line_start(source: &[u8], offset: usize) -> usize { + source[..offset] + .iter() + .rposition(|byte| *byte == b'\n') + .map_or(0, |at| at + 1) +} + +/// The end of the line byte `offset` falls on, before its terminator. +fn line_end(source: &[u8], offset: usize) -> usize { + source[offset..] + .iter() + .position(|byte| *byte == b'\n') + .map_or(source.len(), |at| offset + at) +} + +/// Up to `count` whole lines ending just before `start`, in source order. +fn preceding(source: &[u8], start: usize, count: usize) -> Vec<&[u8]> { + let mut lines = Vec::new(); + let mut at = start; + while lines.len() < count && at > 0 { + // `at` is a line start, so the byte before it is the terminator of the + // line being collected. + let end = at - 1; + let begin = line_start(source, end); + lines.push(&source[begin..end]); + at = begin; + } + lines.reverse(); + lines +} + +/// Up to `count` whole lines starting just after `end`. +fn following(source: &[u8], end: usize, count: usize) -> Vec<&[u8]> { + let mut lines = Vec::new(); + let mut at = end; + while lines.len() < count && at < source.len() { + // Step over the terminator `end` stopped in front of. A file whose last + // line ends in one has nothing after it, and the loop ends here. + at += 1; + if at >= source.len() { + break; + } + let stop = line_end(source, at); + lines.push(&source[at..stop]); + at = stop; + } + lines +} + +/// One decision about one comment. +enum Answer { + Yes, + No, + AllInFile, + NoneInFile, + /// Stop asking and apply what was accepted. + Stop, + /// Throw the whole run away. + Abort, + Help, +} + +/// Put the question and read one answer, explaining itself and asking again +/// until the reader gives one. +/// +/// The answer is read as bytes rather than as a line of text: a terminal can +/// deliver anything, and a stray byte is a typo to ask about again, not an I/O +/// failure that ends a run somebody is in the middle of. +fn ask( + input: &mut dyn BufRead, + output: &mut dyn Write, + presentation: &Presentation, +) -> Result { + loop { + wrote(write!(output, "{PROMPT}"))?; + // The question ends without a newline, so it has to be pushed out by + // hand before the run blocks waiting for the answer to it. + wrote(output.flush())?; + let mut line = Vec::new(); + // Nothing left to read is a reader who is no longer there to answer, + // which is the one answer that must not be guessed at. + if input.read_until(b'\n', &mut line)? == 0 { + return Ok(Answer::Abort); + } + match parse(&String::from_utf8_lossy(&line)) { + Some(Answer::Help) => { + for entry in HELP { + wrote(writeln!(output, "{}", dimmed(entry, presentation)))?; + } + } + Some(answer) => return Ok(answer), + None => wrote(writeln!(output, "{}", dimmed(UNKNOWN, presentation)))?, + } + } +} + +/// Commentary beside the question, told apart from it by being dimmed. +fn dimmed(text: &str, presentation: &Presentation) -> String { + format!( + "{}{text}{}", + color("\x1b[2m", presentation.color), + color("\x1b[0m", presentation.color) + ) +} + +/// The answer one typed line stands for, or `None` for anything else. +fn parse(line: &str) -> Option { + match line.trim().to_ascii_lowercase().as_str() { + "y" | "yes" => Some(Answer::Yes), + "n" | "no" => Some(Answer::No), + "a" => Some(Answer::AllInFile), + "d" => Some(Answer::NoneInFile), + "q" => Some(Answer::Stop), + "x" => Some(Answer::Abort), + "?" | "h" | "help" => Some(Answer::Help), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ocomment_core::{Language, Layout, TransformOptions, apply_edits, transform}; + use std::{io::Cursor, path::PathBuf}; + + /// Two removable block comments, one line, one file. + const TWO: &str = "a/* one */b/* two */c\n"; + + fn file(name: &str, text: &str) -> ProcessedFile { + let source = text.as_bytes().to_vec(); + let result = transform(&source, Language::C, TransformOptions::default()); + ProcessedFile { + path: PathBuf::from(name), + source, + language: Language::C, + result, + } + } + + /// Drive `select` with a scripted answer per line and collect everything it + /// wrote to the terminal. + fn ask(files: &[ProcessedFile], script: &str) -> (Selection, String) { + let mut input = Cursor::new(script.as_bytes().to_vec()); + let mut written: Vec = Vec::new(); + let selection = select(files, &mut input, &mut written, &Presentation::default()).unwrap(); + (selection, String::from_utf8(written).unwrap()) + } + + fn replacement(selection: &Selection) -> String { + assert_eq!(selection.plans.len(), 1, "expected exactly one write plan"); + String::from_utf8(selection.plans[0].replacement.clone()).unwrap() + } + + /// The answers apply to one comment each: the accepted span is gone and the + /// declined one is still in the bytes that would be written. + #[test] + fn yes_and_no_apply_only_the_accepted_comment() { + let (selection, _) = ask(&[file("a.c", TWO)], "y\nn\n"); + assert_eq!((selection.accepted, selection.declined), (1, 1)); + assert!(!selection.aborted); + assert_eq!(replacement(&selection), "a b/* two */c\n"); + assert_eq!(selection.plans[0].path, PathBuf::from("a.c")); + assert_eq!(selection.plans[0].original, TWO.as_bytes()); + } + + /// The question says which comment it is about — where it starts, what kind + /// it is, and how far through the file and the run it sits — and shows the + /// line as it stands against the line the answer would leave behind. + #[test] + fn the_prompt_names_the_comment_and_shows_the_hunk() { + let (_, transcript) = ask(&[file("a.c", TWO)], "y\nn\n"); + assert!( + transcript.contains("a.c:1:2 block comment (1 of 2 in file, 1 of 2 total)\n"), + "the first question did not name its comment:\n{transcript}" + ); + assert!( + transcript.contains("a.c:1:12 block comment (2 of 2 in file, 2 of 2 total)\n"), + "the second question did not name its comment:\n{transcript}" + ); + assert!( + transcript.contains("-a/* one */b/* two */c\n+a b/* two */c\n"), + "the first question did not show the line it would rewrite:\n{transcript}" + ); + assert!( + transcript.contains("-a/* one */b/* two */c\n+a/* one */b c\n"), + "the second question did not show the line it would rewrite:\n{transcript}" + ); + assert_eq!( + transcript.matches("Remove? [y,n,a,d,q,x,?] ").count(), + 2, + "one question was asked per comment:\n{transcript}" + ); + } + + /// Three lines either side of the comment are shown unprefixed, so the + /// reader can tell what the line is doing before answering for it. + #[test] + fn the_hunk_carries_three_lines_of_context_on_each_side() { + let source = "1\n2\n3\n4\n5\nx/* c */y\n6\n7\n8\n9\n10\n"; + let (_, transcript) = ask(&[file("a.c", source)], "n\n"); + assert!( + transcript.contains(" 3\n 4\n 5\n-x/* c */y\n+x y\n 6\n 7\n 8\n"), + "the hunk is not three lines of context around the change:\n{transcript}" + ); + assert!( + !transcript.contains(" 2\n"), + "the hunk reached a fourth line above the change:\n{transcript}" + ); + assert!( + !transcript.contains(" 9\n"), + "the hunk reached a fourth line below the change:\n{transcript}" + ); + } + + /// A block comment 27 lines tall, with a line of source either side. + fn tall(lines: usize) -> String { + let mut text = String::from("before\n/* comment line 1\n"); + for line in 2..lines { + text.push_str(&format!(" * comment line {line}\n")); + } + text.push_str(&format!(" * comment line {lines} */\nafter\n")); + text + } + + /// A comment tall enough to fill the screen would push the question off it. + /// Both sides of the change are capped at `CONTEXT` lines each end, with one + /// marker standing for everything folded away, so the prompt stays in view. + #[test] + fn a_tall_hunk_is_capped_on_both_sides() { + let source = tall(27); + let (_, transcript) = ask(&[file("a.c", &source)], "n\n"); + let removed = transcript + .lines() + .filter(|line| line.starts_with('-')) + .count(); + let added = transcript + .lines() + .filter(|line| line.starts_with('+')) + .count(); + assert!( + removed <= 2 * CONTEXT + 1, + "the removed side printed {removed} lines:\n{transcript}" + ); + assert!( + added <= 2 * CONTEXT + 1, + "the added side printed {added} lines:\n{transcript}" + ); + assert!( + transcript.contains("-/* comment line 1\n"), + "the removed side lost the first line of the comment:\n{transcript}" + ); + assert!( + transcript.contains("- * comment line 27 */\n"), + "the removed side lost the last line of the comment:\n{transcript}" + ); + assert!( + transcript.contains("more line"), + "a capped hunk did not say how much it folded away:\n{transcript}" + ); + assert!( + transcript.contains("Remove? [y,n,a,d,q,x,?] "), + "the question never arrived:\n{transcript}" + ); + } + + /// A change that fits is shown whole: nothing is folded and nothing says it + /// was. + #[test] + fn a_short_hunk_is_shown_whole() { + let (_, transcript) = ask(&[file("a.c", TWO)], "n\nn\n"); + assert!( + !transcript.contains("more line"), + "a hunk that fits was capped anyway:\n{transcript}" + ); + assert!( + transcript.contains("-a/* one */b/* two */c\n+a b/* two */c\n"), + "the whole change was not shown:\n{transcript}" + ); + } + + /// `a` answers for the rest of the file at once and asks nothing more about + /// it; the next file starts asking again. + #[test] + fn a_removes_the_rest_of_the_file_without_asking() { + let files = [file("a.c", TWO), file("b.c", TWO)]; + let (selection, transcript) = ask(&files, "a\ny\nn\n"); + assert_eq!((selection.accepted, selection.declined), (3, 1)); + assert_eq!(selection.plans.len(), 2); + assert_eq!( + String::from_utf8(selection.plans[0].replacement.clone()).unwrap(), + "a b c\n" + ); + assert_eq!( + String::from_utf8(selection.plans[1].replacement.clone()).unwrap(), + "a b/* two */c\n" + ); + assert_eq!( + transcript.matches("Remove? [y,n,a,d,q,x,?] ").count(), + 3, + "`a` kept asking about the file it answered for:\n{transcript}" + ); + } + + /// `d` is the same for the other answer: nothing in the file is removed, so + /// the file has no plan at all. + #[test] + fn d_keeps_the_rest_of_the_file_without_asking() { + let files = [file("a.c", TWO), file("b.c", TWO)]; + let (selection, transcript) = ask(&files, "d\ny\nn\n"); + assert_eq!((selection.accepted, selection.declined), (1, 3)); + assert_eq!(selection.plans.len(), 1); + assert_eq!(selection.plans[0].path, PathBuf::from("b.c")); + assert_eq!( + transcript.matches("Remove? [y,n,a,d,q,x,?] ").count(), + 3, + "`d` kept asking about the file it answered for:\n{transcript}" + ); + } + + /// `q` stops the run where it stands and keeps what was already accepted. + #[test] + fn q_applies_what_was_accepted_and_stops_asking() { + let files = [file("a.c", TWO), file("b.c", TWO)]; + let (selection, transcript) = ask(&files, "y\nq\ny\n"); + assert_eq!(selection.accepted, 1); + assert!(!selection.aborted); + assert_eq!(replacement(&selection), "a b/* two */c\n"); + assert_eq!(selection.plans[0].path, PathBuf::from("a.c")); + assert_eq!( + transcript.matches("Remove? [y,n,a,d,q,x,?] ").count(), + 2, + "`q` asked another question:\n{transcript}" + ); + } + + /// `x` throws the run away, accepted answers included. + #[test] + fn x_writes_nothing() { + let files = [file("a.c", TWO), file("b.c", TWO)]; + let (selection, _) = ask(&files, "y\nx\ny\n"); + assert!(selection.aborted); + assert!( + selection.plans.is_empty(), + "an aborted run still produced something to write" + ); + } + + /// A closed input is a reader who is no longer there to answer, which is + /// the one answer that cannot be guessed at: it aborts. + #[test] + fn end_of_input_aborts_like_x() { + let (selection, _) = ask(&[file("a.c", TWO)], "y\n"); + assert!(selection.aborted, "the second question ran out of input"); + assert!(selection.plans.is_empty()); + } + + /// `?` is not an answer; it explains the answers and asks again. + #[test] + fn help_is_shown_and_the_question_repeated() { + let (selection, transcript) = ask(&[file("a.c", TWO)], "?\nn\nn\n"); + assert_eq!((selection.accepted, selection.declined), (0, 2)); + assert!( + transcript.contains("a - remove it and every remaining comment in this file"), + "`?` did not explain the answers:\n{transcript}" + ); + assert_eq!( + transcript.matches("Remove? [y,n,a,d,q,x,?] ").count(), + 3, + "`?` did not ask the first question again:\n{transcript}" + ); + } + + /// Anything else is a typo, not a decision, and is never taken for one. + #[test] + fn an_unknown_answer_re_prompts() { + let (selection, transcript) = ask(&[file("a.c", TWO)], "z\n\ny\nn\n"); + assert_eq!((selection.accepted, selection.declined), (1, 1)); + assert_eq!(replacement(&selection), "a b/* two */c\n"); + assert!( + transcript.contains("unknown answer"), + "an unknown answer went unremarked:\n{transcript}" + ); + assert_eq!( + transcript.matches("Remove? [y,n,a,d,q,x,?] ").count(), + 4, + "an unknown answer did not ask again:\n{transcript}" + ); + } + + /// Under `layout = "columns"` a removal is replaced by exactly as many + /// display columns as the comment occupied, and every such replacement is + /// measured from the *source*, not from whatever earlier removals left + /// behind. That is what lets this command apply a subset of the edits a + /// transform produced: a width-preserving replacement moves nothing, so + /// each remaining comment still begins at the display column its own + /// replacement was computed for. + /// + /// Pinned by transforming the partially edited bytes again and requiring + /// the replacement to come out byte-identical to the one the full transform + /// computed — the tab inside the second comment makes that replacement + /// depend on the column it starts at. + #[test] + fn partial_column_edits_keep_the_replacement_the_transform_computed() { + let source = b"x/* one */y/* a\tb */z\n"; + let options = TransformOptions { + layout: Layout::Columns, + ..TransformOptions::default() + }; + let full = transform(source, Language::C, options.clone()); + assert_eq!(full.edits.len(), 2, "the fixture lost a comment"); + + for taken in [0usize, 1] { + let kept = 1 - taken; + let partial = apply_edits(source, std::slice::from_ref(&full.edits[taken])); + let again = transform(&partial, Language::C, options.clone()); + assert_eq!( + again.edits.len(), + 1, + "the partially edited source lost the comment that was kept" + ); + assert_eq!( + again.edits[0].replacement, full.edits[kept].replacement, + "applying edit #{taken} on its own changed what edit #{kept} replaces" + ); + assert_eq!( + again.output, full.output, + "applying edit #{taken} and then the rest is not the whole transform" + ); + } + + let both: Vec = full.edits.clone(); + assert_eq!(apply_edits(source, &both), full.output); + } +} diff --git a/rust/ocomment/src/main.rs b/rust/ocomment/src/main.rs index a6fd7e8..c8dd3a9 100644 --- a/rust/ocomment/src/main.rs +++ b/rust/ocomment/src/main.rs @@ -3,6 +3,7 @@ mod cli; mod config; mod files; mod git; +mod interactive; mod lsp; mod output; mod plugin; diff --git a/rust/ocomment/src/output.rs b/rust/ocomment/src/output.rs index f7c49e7..6eda74d 100644 --- a/rust/ocomment/src/output.rs +++ b/rust/ocomment/src/output.rs @@ -1,7 +1,13 @@ -use crate::files::{NO_LANGUAGE, SkippedFile}; +use crate::{ + config::PolicyTrace, + files::{NO_LANGUAGE, SkippedFile}, +}; use anyhow::Result; use clap::ValueEnum; -use ocomment_core::{ByteSpan, CommentKind, Disposition, Language, Policy, TransformResult}; +use ocomment_core::{ + ByteSpan, Comment, CommentKind, Disposition, DispositionExplanation, DispositionPatterns, + Language, Policy, ScanOptions, TransformResult, explain_disposition_with, +}; use serde::Serialize; use serde_json::{Value, json}; use similar::{ChangeTag, TextDiff}; @@ -56,8 +62,8 @@ pub struct RenderOptions { pub verbosity: Verbosity, /// Human lines carry a one-line rendering of the comment text. pub preview: bool, - // Plumbed for the presentation work that follows; nothing reads it yet. - #[allow(dead_code)] + /// Human `check` and `scan` lines carry every comment, kept ones included, + /// each under an indented line naming the rule that decided it. pub explain: bool, /// The run is `fix --dry-run`: it produces the diff but speaks the /// vocabulary of the `fix` it is standing in for. @@ -223,7 +229,120 @@ pub fn removable_label(kind: CommentKind) -> String { /// The one-line label for a comment OComment deliberately protects. pub fn kept_label(kind: CommentKind, reason: &str) -> String { - format!("kept {kind} comment: {reason}") + format!("{}: {reason}", kept_prefix(kind)) +} + +/// The same label without a reason, for a report that gives the reason on a +/// line of its own. +fn kept_prefix(kind: CommentKind) -> String { + format!("kept {kind} comment") +} + +/// What `--explain` needs to account for one file's comments: the options its +/// scan actually ran with, and where each of their settings came from. +#[derive(Clone, Debug)] +pub struct FileExplanation { + pub options: ScanOptions, + pub trace: PolicyTrace, +} + +/// That material for the files of one run, under the path the run reports each +/// file by. A run that was not asked to explain anything carries none. +pub type Explanations = BTreeMap; + +/// One file's explanation material with its policy patterns already compiled. +/// +/// The two regex sets are the same for every comment in the file, so they are +/// built once when the file is reached rather than once per reported line. +struct Explainer<'a> { + material: &'a FileExplanation, + patterns: DispositionPatterns, +} + +impl<'a> Explainer<'a> { + /// An unparseable pattern list is ignored here as the scanner ignores it, + /// which is exactly what `explain_disposition` falls back to on its own. + fn new(material: &'a FileExplanation) -> Self { + Self { + patterns: DispositionPatterns::compile(&material.options) + .unwrap_or_else(|_| DispositionPatterns::empty()), + material, + } + } +} + +/// The indented line under one reported comment: the rule that decided its +/// fate, and either the setting behind that rule or the flag that would +/// overrule it. +/// +/// The pattern a regex explanation quotes and the globs a source names were +/// both written by whoever wrote the configuration, so the composed line gets a +/// comment preview's treatment before it reaches a terminal: one line, no +/// control sequences. The width is not capped — a line that ends in an ellipsis +/// where the pattern was answers nothing. +fn explanation_line( + file: &ProcessedFile, + comment: &Comment, + explainer: &Explainer<'_>, + options: &RenderOptions, +) -> String { + let material = explainer.material; + let start = comment.span.start.min(file.source.len()); + let end = comment.span.end.clamp(start, file.source.len()); + let verdict = explain_disposition_with( + &explainer.patterns, + comment.kind, + &file.source[start..end], + file.language, + &material.options, + ); + let tail = match material.trace.origin_of(&verdict, &material.options) { + Some(origin) => format!(" ({origin})"), + None => next_step(&verdict), + }; + format!( + " {}{}{}", + color("\x1b[2m", options.presentation.color), + fold(&format!("{verdict}{tail}")), + color("\x1b[0m", options.presentation.color) + ) +} + +/// Write that line under the comment it is about, when the run has the +/// material to account for it. +fn explain_comment( + output: &mut impl Write, + file: &ProcessedFile, + comment: &Comment, + explainer: Option<&Explainer<'_>>, + options: &RenderOptions, +) -> Result<()> { + let Some(explainer) = explainer else { + return Ok(()); + }; + wrote(writeln!( + output, + "{}", + explanation_line(file, comment, explainer, options) + )) +} + +/// How to overrule a built-in rule, which no setting decided and no table can +/// be pointed at for. +fn next_step(verdict: &DispositionExplanation) -> String { + match verdict { + DispositionExplanation::ProtectedPreamble => { + "; add --force-protected to remove it".to_owned() + } + DispositionExplanation::KeptHtml => format!( + "; use --remove-kind {} or --policy all to remove it", + CommentKind::HtmlComment + ), + DispositionExplanation::KeptDirective { kind, .. } => { + format!("; use --remove-kind {kind} or --policy all to remove it") + } + _ => String::new(), + } } /// How many display columns a comment preview may occupy. @@ -238,7 +357,10 @@ const PREVIEW_COLUMNS: usize = 72; fn preview(source: &[u8], span: ByteSpan, max_columns: usize) -> String { let start = span.start.min(source.len()); let end = span.end.clamp(start, source.len()); - fold(&String::from_utf8_lossy(&source[start..end]), max_columns) + truncate( + fold(&String::from_utf8_lossy(&source[start..end])), + max_columns, + ) } /// The same treatment for a line that did not come out of a source file. @@ -248,11 +370,56 @@ fn preview(source: &[u8], span: ByteSpan, max_columns: usize) -> String { /// tool planted there could otherwise clear the screen or repaint the report /// from its own version line. pub(crate) fn sanitize_line(text: &str) -> String { - fold(text, PREVIEW_COLUMNS) + truncate(fold(text), PREVIEW_COLUMNS) } -/// Fold `text` onto one control-free line of at most `max_columns` columns. -fn fold(text: &str, max_columns: usize) -> String { +/// The same treatment for a line that must not be cut short. +/// +/// A directory name is chosen by whoever made the directory, so the rows +/// `doctor` prints one on are untrusted for the same reason a version line is. +/// What they are not is commentary: an absolute path is easily longer than a +/// comment preview may be, and a row that ends in an ellipsis where the reader +/// was looking for the rest of the path answers nothing. Only the width cap is +/// dropped; every control character is still replaced. +pub(crate) fn sanitize_path(text: &str) -> String { + fold(text) +} + +/// The same treatment for a line of source a prompt has to show as code. +/// +/// A hunk is read for its shape as much as for its text — indentation says +/// what a line belongs to — so unlike a comment preview this one keeps the +/// spaces it was given and expands a tab onto the same eight-column stop the +/// `columns` layout measures a replacement by. What it does not keep is +/// anything that drives the terminal: every control character, `ESC` and the +/// bidirectional overrides above all, still becomes U+FFFD, and the result is +/// still one line cut to a fixed width, because the question underneath it has +/// to stay on the screen with it. +pub(crate) fn sanitize_source_line(text: &str) -> String { + let mut line = String::with_capacity(text.len()); + let mut column = 0usize; + for character in text.chars() { + if character == '\t' { + let width = TAB_WIDTH - (column % TAB_WIDTH); + line.extend(std::iter::repeat_n(' ', width)); + column += width; + } else if is_control(character) { + line.push('\u{fffd}'); + column += 1; + } else { + line.push(character); + column += columns(character); + } + } + truncate(line, PREVIEW_COLUMNS) +} + +/// The tab stop `sanitize_source_line` expands to, the one the `columns` +/// layout already measures a tab by. +const TAB_WIDTH: usize = 8; + +/// Fold `text` onto one control-free line. +fn fold(text: &str) -> String { let mut folded = String::with_capacity(text.len()); let mut pending_space = false; for character in text.chars() { @@ -272,7 +439,7 @@ fn fold(text: &str, max_columns: usize) -> String { character }); } - truncate(folded, max_columns) + folded } /// C0, DEL, C1, and the bidirectional and separator format controls. None of @@ -431,10 +598,21 @@ pub fn render( files: &[ProcessedFile], skipped: &[SkippedFile], options: &RenderOptions, +) -> Result<()> { + render_explained(files, skipped, options, &Explanations::new()) +} + +/// The same report, with the material `--explain` needs for the files it has +/// it for. A file with none is reported exactly as `render` reports it. +pub fn render_explained( + files: &[ProcessedFile], + skipped: &[SkippedFile], + options: &RenderOptions, + explanations: &Explanations, ) -> Result<()> { let mut output = stdout(); match options.format { - OutputFormat::Human => render_human(&mut output, files, skipped, options), + OutputFormat::Human => render_human(&mut output, files, skipped, options, explanations), OutputFormat::Json => render_json(&mut output, files, skipped), OutputFormat::Jsonl => render_jsonl(&mut output, files, skipped), OutputFormat::Sarif => render_sarif(&mut output, files, skipped), @@ -448,6 +626,7 @@ fn render_human( files: &[ProcessedFile], skipped: &[SkippedFile], options: &RenderOptions, + explanations: &Explanations, ) -> Result<()> { let operation = options.operation; let presentation = options.presentation; @@ -477,6 +656,12 @@ fn render_human( diagnostic.message ))?; } + let explainer = options + .explain + .then(|| explanations.get(&file.path)) + .flatten() + .map(Explainer::new); + let explainer = explainer.as_ref(); if operation == Operation::Scan { // The listing is the product of `scan`; `-q` keeps it too. for comment in &file.result.report.comments { @@ -491,6 +676,7 @@ fn render_human( comment.span.end, preview_suffix(&file.source, comment.span, options) ))?; + explain_comment(output, file, comment, explainer, options)?; } } else if quiet { continue; @@ -504,41 +690,36 @@ fn render_human( ))?; } } else { - for comment in file - .result - .report - .comments - .iter() - .filter(|comment| comment.disposition.is_remove()) - { + // `check` reports what it would remove. Asked to explain itself it + // reports the rest too, because a comment it left alone is exactly + // the one the reader is asking about. + for comment in &file.result.report.comments { + let removable = comment.disposition.is_remove(); + if !options.explain && !removable { + continue; + } let (line, column) = line_column(&file.source, comment.span.start); wrote(writeln!( output, "{}:{line}:{column}: {}{}{}{}", display_path(&file.path, presentation.hyperlinks), - color("\x1b[33m", presentation.color), - removable_label(comment.kind), + color( + if removable { "\x1b[33m" } else { "\x1b[32m" }, + presentation.color + ), + if removable { + removable_label(comment.kind) + } else { + kept_prefix(comment.kind) + }, color("\x1b[0m", presentation.color), preview_suffix(&file.source, comment.span, options) ))?; + explain_comment(output, file, comment, explainer, options)?; } } } - // The skips this run has to name, in one wording for whichever stream ends - // up carrying them. An I/O error is named however quiet the run was asked - // to be: it is a failure, not commentary. - let skips: Vec = skipped - .iter() - .filter(|item| item.error || (!quiet && (item.explicit || verbose))) - .map(|item| { - format!( - "{}: {}: {}", - display_path(&item.path, presentation.hyperlinks), - if item.error { "error" } else { "skipped" }, - item.reason - ) - }) - .collect(); + let skips = skip_lines(skipped, presentation, options.verbosity); // `diff` keeps standard output for the patch alone, so the skips it met // are left to standard error. `fix --dry-run` is that same `diff` speaking // for the `fix` it stands in for: a skipped path can be the whole answer @@ -607,6 +788,78 @@ fn render_human( Ok(()) } +/// The skips one run has to name, in one wording for whichever stream ends up +/// carrying them. An I/O error is named however quiet the run was asked to be: +/// it is a failure, not commentary. +/// +/// Shared with `fix --interactive`, which writes no report of its own and would +/// otherwise be the one command that never says why it passed a file over. +pub(crate) fn skip_lines( + skipped: &[SkippedFile], + presentation: Presentation, + verbosity: Verbosity, +) -> Vec { + let quiet = verbosity == Verbosity::Quiet; + let verbose = verbosity == Verbosity::Verbose; + skipped + .iter() + .filter(|item| item.error || (!quiet && (item.explicit || verbose))) + .map(|item| { + format!( + "{}: {}: {}", + display_path(&item.path, presentation.hyperlinks), + if item.error { "error" } else { "skipped" }, + item.reason + ) + }) + .collect() +} + +/// The numbers an interactive run's verdict is built from. +/// +/// They count answers rather than findings, which is the one thing the ordinary +/// summary cannot say: it counts what a run *could* have removed. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct InteractiveOutcome { + /// Comments the reader accepted for removal. + pub removed: usize, + /// Questions the reader answered. `a` and `d` answer for every remaining + /// comment in their file, so those count here too. + pub reviewed: usize, + /// Comments the run had to offer, whether or not it got as far as asking. + pub offered: usize, + /// Files an accepted removal is written to. + pub changed: usize, + /// Files the run scanned. + pub scanned: usize, +} + +/// What an interactive run came to, in the vocabulary every other summary uses. +/// +/// A run with nothing to offer borrows the wording the plain `fix` summary +/// gives the same answer, because the only number worth reporting there is how +/// much was looked at. A run stopped by `q` is counted against the questions it +/// actually asked, and says how many it never got to: measuring the acceptances +/// against every comment the run *could* have offered would read as a pile of +/// refusals nobody made. +pub(crate) fn interactive_summary(outcome: InteractiveOutcome) -> String { + if outcome.offered == 0 { + return format!("Nothing to fix in {}.", plural(outcome.scanned, "file")); + } + let unreviewed = outcome.offered.saturating_sub(outcome.reviewed); + let tail = if unreviewed == 0 { + String::new() + } else { + format!(" ({} not reviewed)", comments(unreviewed, "")) + }; + format!( + "Removed {} of {} in {}{tail}.", + outcome.removed, + comments(outcome.reviewed, ""), + plural(outcome.changed, "file") + ) +} + /// The whole end-of-run summary: the verdict for the run, the folded skips, /// and the I/O errors that were listed one by one above it. fn summary_report(summary: &Summary, options: &RenderOptions, folded: bool) -> String { @@ -757,7 +1010,7 @@ fn kind_breakdown(files: &[ProcessedFile], options: &RenderOptions) -> Option &'static str { +pub(crate) fn color(code: &'static str, enabled: bool) -> &'static str { if enabled { code } else { "" } } @@ -995,7 +1248,7 @@ pub fn unified_diff(path: &Path, original: &[u8], transformed: &[u8]) -> String output } -fn line_column(source: &[u8], offset: usize) -> (usize, usize) { +pub(crate) fn line_column(source: &[u8], offset: usize) -> (usize, usize) { let offset = offset.min(source.len()); let mut line = 1usize; let mut start = 0usize; @@ -1127,6 +1380,112 @@ mod tests { assert!(rendered.ends_with('\u{2026}'), "truncation is unmarked"); } + /// A hunk is read as code, so the indentation that says what a line belongs + /// to survives — but nothing that drives the terminal does, because the + /// prompt asking about that line sits directly underneath it. + #[test] + fn a_source_line_keeps_its_shape_and_loses_its_control_characters() { + assert_eq!( + sanitize_source_line(" let x = 1; // note"), + " let x = 1; // note", + "the indentation of a shown line was collapsed" + ); + assert_eq!( + sanitize_source_line("\tif (x) {"), + " if (x) {", + "a tab did not reach its eight-column stop" + ); + assert_eq!( + sanitize_source_line("a\u{1b}[2Jb\u{202e}c"), + "a\u{fffd}[2Jb\u{fffd}c", + "an escape sequence reached the terminal verbatim" + ); + let capped = sanitize_source_line(&"v".repeat(PREVIEW_COLUMNS * 3)); + let width: usize = capped + .chars() + .map(|ch| unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0)) + .sum(); + assert!( + width <= PREVIEW_COLUMNS, + "a shown line ran to {width} columns and pushed the question off the screen" + ); + } + + /// The interactive verdict counts answers, and every noun agrees with the + /// number in front of it. + #[test] + fn the_interactive_summary_pluralizes_both_of_its_nouns() { + assert_eq!( + interactive_summary(InteractiveOutcome { + removed: 1, + reviewed: 1, + offered: 1, + changed: 1, + scanned: 1, + }), + "Removed 1 of 1 comment in 1 file." + ); + assert_eq!( + interactive_summary(InteractiveOutcome { + removed: 2, + reviewed: 5, + offered: 5, + changed: 3, + scanned: 4, + }), + "Removed 2 of 5 comments in 3 files." + ); + } + + /// A run that was never asked a question says so in the vocabulary the + /// plain `fix` summary uses for the same answer, and counts the files it + /// scanned — `Removed 0 of 0 comments in 0 files` named three numbers, none + /// of which was the one the reader wanted. + #[test] + fn an_interactive_run_with_nothing_to_offer_borrows_the_fix_wording() { + assert_eq!( + interactive_summary(InteractiveOutcome { + scanned: 3, + ..InteractiveOutcome::default() + }), + "Nothing to fix in 3 files." + ); + assert_eq!( + interactive_summary(InteractiveOutcome { + scanned: 1, + ..InteractiveOutcome::default() + }), + "Nothing to fix in 1 file." + ); + } + + /// `q` stops the questions, so the verdict counts the ones that were + /// answered and says how many were left unasked. Reporting `1 of 9` to a + /// reader who answered twice would read as seven refusals. + #[test] + fn a_stopped_interactive_run_counts_the_questions_it_asked() { + assert_eq!( + interactive_summary(InteractiveOutcome { + removed: 1, + reviewed: 2, + offered: 9, + changed: 1, + scanned: 4, + }), + "Removed 1 of 2 comments in 1 file (7 comments not reviewed)." + ); + assert_eq!( + interactive_summary(InteractiveOutcome { + removed: 0, + reviewed: 1, + offered: 2, + changed: 0, + scanned: 1, + }), + "Removed 0 of 1 comment in 0 files (1 comment not reviewed)." + ); + } + /// What a probed tool says about itself gets the preview's treatment: one /// line, no control sequences, and no more of it than a preview shows. #[test] diff --git a/rust/ocomment/tests/cli.rs b/rust/ocomment/tests/cli.rs index 6691b8a..92d1bf7 100644 --- a/rust/ocomment/tests/cli.rs +++ b/rust/ocomment/tests/cli.rs @@ -627,15 +627,158 @@ fn explicit_io_failure_returns_two_and_blocks_the_whole_fix() { assert!(String::from_utf8_lossy(&output.stdout).contains("path does not exist")); } +/// A command that names no path checks the current directory, the way every +/// other file-walking developer tool does. The repository root is still where +/// the configuration is discovered and where the override globs are anchored, +/// but it is no longer what a bare `ocomment` walks: run from a subdirectory, +/// the command must not reach back up to files the caller cannot see. #[test] -fn no_argument_scan_uses_repository_root_from_a_subdirectory() { +fn no_argument_scan_uses_the_current_directory_not_the_repository_root() { let directory = repository(); fs::write(directory.path().join("root.rs"), b"// root comment\n").unwrap(); let nested = directory.path().join("nested/deeper"); fs::create_dir_all(&nested).unwrap(); + fs::write(nested.join("deep.rs"), b"// deep comment\n").unwrap(); + let output = run(&nested, &[]); assert_eq!(output.status.code(), Some(1)); - assert!(String::from_utf8_lossy(&output.stdout).contains("root.rs")); + let report = String::from_utf8(output.stdout).unwrap(); + assert!( + !report.contains("root.rs"), + "the bare command reached above the current directory:\n{report}" + ); + assert!( + report.contains("deep.rs:1:1: removable"), + "the bare command never checked the current directory:\n{report}" + ); + // The implicit target is `.`, and a walk rooted there prefixes every entry + // with `./`. `ocomment` and `ocomment check deep.rs` report one file under + // one name, so that prefix is not part of it. + assert!( + !report.contains("./"), + "the implicit target leaked its `./` into the report:\n{report}" + ); + + // `-v` names both halves of the answer: the root the configuration came + // from, and the target that root no longer decides. + let traced = run(&nested, &["-v"]); + let trace = String::from_utf8(traced.stderr).unwrap(); + let repository_name = directory.path().file_name().unwrap().to_str().unwrap(); + assert!( + trace + .lines() + .any(|line| line.starts_with("root: ") && line.ends_with(repository_name)), + "the trace did not root the run at the repository:\n{trace}" + ); + assert!( + trace.lines().any(|line| line == "target: ."), + "the trace did not name the implicit target:\n{trace}" + ); +} + +/// The root keeps the two jobs it did not lose: it is where `.ocomment.toml` +/// is found, and it is what `files.include`, `files.exclude`, and every +/// `[[overrides]].paths` glob is written relative to. A path named on the +/// command line is relative to the working directory instead, so the two only +/// line up once a path is resolved against the directory it was typed in — +/// whichever of the three ways the file was named. +#[test] +fn project_config_and_overrides_apply_from_a_subdirectory() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join(".ocomment.toml"), + b"version = 1\n\n[files]\nexclude = [\"nested/skip/**\"]\n\n[[overrides]]\npaths = [\"nested/**\"]\npolicy = \"all\"\n", + ) + .unwrap(); + let nested = directory.path().join("nested"); + fs::create_dir_all(nested.join("skip")).unwrap(); + // A directive is kept under the default `safe` policy and removed under + // `all`, so the line it is reported on is the override speaking. + fs::write(nested.join("kept.rs"), b"let x = 1; // rustfmt::skip\n").unwrap(); + fs::write(nested.join("skip/ignored.rs"), b"let y = 2; // remove\n").unwrap(); + + for arguments in [&[][..], &["check", "."][..], &["check", "kept.rs"][..]] { + let output = run(&nested, arguments); + let report = String::from_utf8(output.stdout).unwrap(); + assert_eq!( + output.status.code(), + Some(1), + "`ocomment {}` did not apply the override:\n{report}", + arguments.join(" ") + ); + assert!( + report.contains("kept.rs:1:12: removable directive comment"), + "`ocomment {}` did not apply the override:\n{report}", + arguments.join(" ") + ); + assert!( + !report.contains("ignored.rs"), + "`ocomment {}` walked into the excluded directory:\n{report}", + arguments.join(" ") + ); + } +} + +/// `fix` is the command that writes, so the change of target matters most +/// there: run from a subdirectory it rewrites that subdirectory, and the +/// files above it are none of its business. +#[test] +fn fix_from_a_subdirectory_leaves_the_repository_root_alone() { + let directory = repository(); + let untouched = directory.path().join("root.rs"); + let original = b"let a = 1; // root comment\n"; + fs::write(&untouched, original).unwrap(); + let nested = directory.path().join("nested"); + fs::create_dir(&nested).unwrap(); + let rewritten = nested.join("deep.rs"); + fs::write(&rewritten, b"let b = 2; // deep comment\n").unwrap(); + + let output = run(&nested, &["fix"]); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + fs::read(&untouched).unwrap(), + original, + "`fix` from a subdirectory rewrote the repository root" + ); + assert_eq!(fs::read(&rewritten).unwrap(), b"let b = 2; \n"); +} + +/// A reader who has only ever run `ocomment fix` from the top of a repository +/// can read the bare command as "fix the project", so the one run that writes +/// says where it is pointed and where the project it belongs to starts. From +/// the root the two are the same directory and the note would be noise. +#[test] +fn fix_from_a_subdirectory_notes_the_project_root() { + let directory = repository(); + let nested = directory.path().join("nested"); + fs::create_dir(&nested).unwrap(); + fs::write(nested.join("deep.rs"), b"let b = 2; // deep comment\n").unwrap(); + + let output = run(&nested, &["fix"]); + assert_eq!(output.status.code(), Some(0)); + let note = String::from_utf8(output.stderr).unwrap(); + assert!( + note.contains("note: fixing files under . (project root: "), + "`fix` never said what it was pointed at:\n{note}" + ); + assert_eq!( + note.matches("note: fixing files under").count(), + 1, + "the scope was noted more than once:\n{note}" + ); + + let from_root = run(directory.path(), &["fix"]); + assert_eq!(from_root.status.code(), Some(0)); + let quiet = String::from_utf8(from_root.stderr).unwrap(); + assert!( + !quiet.contains("note: fixing files under"), + "the note was printed where the target and the root agree:\n{quiet}" + ); } #[test] @@ -656,6 +799,99 @@ fn explicit_directory_bypasses_hidden_and_size_limits() { assert!(String::from_utf8_lossy(&output.stdout).contains(".hidden.rs")); } +/// The target a command with no PATH stands in for is not an explicitly named +/// one: `.` substituted for a missing argument walks with the ordinary hidden +/// and size limits, so a bare run reports what a run naming its files would. +/// Naming the same directory is a request, and still bypasses both. +#[test] +fn an_implicit_target_keeps_the_hidden_and_size_limits() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join(".ocomment.toml"), + b"version = 1\n[files]\nmax_size = 1000\n", + ) + .unwrap(); + fs::create_dir(directory.path().join(".hidden")).unwrap(); + fs::write( + directory.path().join(".hidden/b.rs"), + b"let b = 1; // hidden\n", + ) + .unwrap(); + fs::create_dir(directory.path().join("src")).unwrap(); + fs::write( + directory.path().join("src/a.rs"), + b"let a = 1; // remove me\n", + ) + .unwrap(); + let mut big = String::from("// oversized\n"); + while big.len() <= 100_000 { + big.push_str("let x = 1;\n"); + } + fs::write(directory.path().join("src/big.rs"), big.as_bytes()).unwrap(); + + let bare = run(directory.path(), &[]); + let stdout = String::from_utf8_lossy(&bare.stdout).into_owned(); + let stderr = String::from_utf8_lossy(&bare.stderr).into_owned(); + assert_eq!( + bare.status.code(), + Some(1), + "bare run said:\n{stdout}{stderr}" + ); + assert!( + stdout.contains("src/a.rs:1:12: removable line comment"), + "a bare run missed the one file it should report:\n{stdout}" + ); + assert!( + !stdout.contains(".hidden"), + "a bare run reached into a hidden directory:\n{stdout}" + ); + assert!( + !stdout.contains("big.rs"), + "a bare run scanned a file over files.max_size:\n{stdout}" + ); + assert!( + stderr.contains("Found 1 removable comment in 1 file (1 file scanned)."), + "a bare run counted more than the one file it may walk:\n{stderr}" + ); + assert!( + stderr.contains("1 file skipped (too large: 1"), + "a bare run did not fold the oversized file into its skips:\n{stderr}" + ); +} + +/// `.git` is hidden, so nothing a bare run does may look inside it — and `fix` +/// least of all: the sample hooks git writes into a fresh repository are full +/// of comments, and rewriting them is not what "fix my project" asked for. +#[test] +fn a_bare_run_never_reaches_into_the_git_directory() { + let directory = tempfile::tempdir().unwrap(); + git(directory.path(), &["init"]); + fs::write(directory.path().join(".ocomment.toml"), b"version = 1\n").unwrap(); + let hook = directory.path().join(".git/hooks/x.sample"); + fs::write(&hook, b"let x = 1; // sample hook comment\n").unwrap(); + let before = fs::read(&hook).unwrap(); + fs::write(directory.path().join("a.rs"), b"let a = 1; // remove me\n").unwrap(); + + let check = run(directory.path(), &[]); + let listing = String::from_utf8_lossy(&check.stdout).into_owned(); + assert!( + !listing.contains(".git"), + "a bare check listed something under .git:\n{listing}" + ); + + let fixed = run(directory.path(), &["fix"]); + let report = String::from_utf8_lossy(&fixed.stdout).into_owned(); + assert!( + !report.contains(".git"), + "a bare fix reported something under .git:\n{report}" + ); + assert_eq!( + fs::read(&hook).unwrap(), + before, + "a bare fix rewrote a file under .git" + ); +} + #[cfg(unix)] #[test] fn symlink_following_is_explicitly_configurable() { @@ -2935,6 +3171,63 @@ fn doctor_reports_the_environment_it_resolved() { ); } +/// A directory name is chosen by whoever made the directory, not by OComment, +/// so the two rows that print one are untrusted input on their way to a +/// terminal exactly like a probed tool's version line. They are sanitised the +/// same way and cut nowhere: a path is the answer the reader came for, and one +/// ending in an ellipsis names no directory at all. +#[cfg(unix)] +#[test] +fn doctor_sanitises_the_directories_it_reports_without_cutting_them_short() { + // Long enough that a preview-width cap would have to cut it, and carrying + // the escape that would let a directory name repaint the report. + let name = format!("ocomment\u{1b}{}", "a".repeat(90)); + let directory = tempfile::Builder::new() + .prefix(&name) + .tempdir() + .expect("a directory name may carry an escape on this platform"); + // A project file of its own makes this directory the root as well, so both + // rows name it and both are pinned by one run. + fs::write(directory.path().join(".ocomment.toml"), b"version = 1\n").unwrap(); + let empty = tempfile::tempdir().unwrap(); + let output = Command::new(binary()) + .current_dir(directory.path()) + .env("PATH", "/usr/bin:/bin") + .env("XDG_CONFIG_HOME", empty.path()) + .arg("doctor") + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + !output.stdout.contains(&0x1b), + "an escape byte reached the report: {:?}", + String::from_utf8_lossy(&output.stdout) + ); + let report = String::from_utf8(output.stdout).unwrap(); + let sanitised = name.replace('\u{1b}', "\u{fffd}"); + for row in ["cwd", "root"] { + let prefix = format!("{row}: "); + let named = report + .lines() + .find(|line| line.starts_with(&prefix)) + .unwrap_or_else(|| panic!("doctor printed no `{row}` row:\n{report}")); + assert!( + named.contains(&sanitised), + "the `{row}` row lost the directory it names:\n{report}" + ); + // A version line may be cut to the preview width; a path may not. + assert!( + !named.contains('\u{2026}'), + "the `{row}` row was cut to the preview width:\n{report}" + ); + } +} + /// The scaffold refuses to write into a directory that already exists, and a /// refusal that only says "refusing" leaves the reader to guess. There are two /// ways out — take the directory away, or take the plugin that owns it away — @@ -3170,3 +3463,384 @@ fn the_checked_in_completions_are_the_ones_the_binary_generates() { ); } } + +/// `--explain` answers "why was this comment kept?": it lists every comment, +/// kept ones included, and names both the rule that decided each one and the +/// table that rule was written in. A plain `check` still reports only what it +/// would remove. +#[test] +fn check_explain_names_the_override_and_the_pattern_that_kept_a_comment() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join(".ocomment.toml"), + b"version = 1\n\n[policy]\nkeep_regex = [\"(?i)^// api\"]\n\n\ + [[overrides]]\npaths = [\"gen/**\"]\nkeep_regex = [\"(?i)generated\"]\n", + ) + .unwrap(); + fs::create_dir(directory.path().join("gen")).unwrap(); + fs::write( + directory.path().join("gen/b.rs"), + b"/* generated */\nlet x = 1; // TODO\n", + ) + .unwrap(); + fs::write(directory.path().join("a.rs"), b"// API stays\n").unwrap(); + + let plain = run(directory.path(), &["check"]); + assert_eq!(plain.status.code(), Some(1)); + let plain = String::from_utf8(plain.stdout).unwrap(); + assert!( + !plain.contains("kept"), + "a plain `check` listed a kept comment:\n{plain}" + ); + + let output = run(directory.path(), &["check", "--explain"]); + assert_eq!( + output.status.code(), + Some(1), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let report = String::from_utf8(output.stdout).unwrap(); + for needle in [ + "gen/b.rs:1:1: kept block comment: /* generated */", + "kept: matched keep_regex #1 `(?i)generated` ([[overrides]] #0, paths = [\"gen/**\"])", + "gen/b.rs:2:12: removable line comment: // TODO", + // Nothing set `[policy] mode`, so the reader is told it is a default + // rather than sent to a file that never mentions it. The pattern the + // same file does set is named with the file, spelled the way the reader + // typed their way into the directory. + "removed: policy `safe` removes ordinary comments (built-in defaults)", + "a.rs:1:1: kept line comment: // API stays", + "kept: matched keep_regex #0 `(?i)^// api` ([policy] in .ocomment.toml)", + ] { + assert!( + report.contains(needle), + "`check --explain` lacks {needle:?}:\n{report}" + ); + } +} + +/// A comment no setting decided is explained by the flag that would change its +/// fate, because there is no table to send the reader to. +#[test] +fn check_explain_says_what_would_remove_a_preamble_or_a_directive() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("script.py"), + b"#!/usr/bin/env python3\n# note\n", + ) + .unwrap(); + fs::write( + directory.path().join("app.js"), + b"// eslint-disable-next-line\nlet x = 1;\n", + ) + .unwrap(); + + let output = run(directory.path(), &["check", "--explain"]); + assert_eq!( + output.status.code(), + Some(1), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let report = String::from_utf8(output.stdout).unwrap(); + for needle in [ + "script.py:1:1: kept shebang comment: #!/usr/bin/env python3", + "required source preamble", + "add --force-protected to remove it", + "app.js:1:1: kept directive comment: // eslint-disable-next-line", + "kept: tool or language directive `eslint`; use --remove-kind directive \ + or --policy all to remove it", + ] { + assert!( + report.contains(needle), + "`check --explain` lacks {needle:?}:\n{report}" + ); + } +} + +/// A setting the command line supplied is named as the command line, not as +/// the file it would otherwise have been written in. +#[test] +fn explain_names_the_command_line_when_a_flag_set_the_policy() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("notice.rs"), + b"// Copyright 2026 Example\nlet x = 1; // TODO\n", + ) + .unwrap(); + + let output = run( + directory.path(), + &["check", "--explain", "--policy", "legal"], + ); + assert_eq!( + output.status.code(), + Some(1), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let report = String::from_utf8(output.stdout).unwrap(); + for needle in [ + "notice.rs:1:1: kept license comment: // Copyright 2026 Example", + "kept: policy legal protects license comments, and this one says `copyright` \ + (--policy on the command line)", + "removed: policy `legal` removes ordinary comments (--policy on the command line)", + ] { + assert!( + report.contains(needle), + "`check --explain --policy legal` lacks {needle:?}:\n{report}" + ); + } +} + +/// The machine formats are schemas, not prose, and none of them has a place to +/// put an explanation. Asking for one is a usage error rather than a flag that +/// quietly does nothing. +#[test] +fn explain_is_refused_by_every_machine_format() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("a.rs"), b"let x = 1; // TODO\n").unwrap(); + for format in ["json", "jsonl", "sarif", "github"] { + let output = run( + directory.path(), + &["check", "--explain", "--format", format], + ); + assert_eq!( + output.status.code(), + Some(2), + "`--format {format} --explain` was accepted" + ); + let error = String::from_utf8_lossy(&output.stderr); + assert!( + error.contains("--explain is only available with --format human"), + "`--format {format} --explain` said:\n{error}" + ); + assert!( + output.stdout.is_empty(), + "`--format {format} --explain` wrote a report anyway" + ); + } +} + +/// `--explain` annotates a report of comments, and only `check` and `scan` +/// write one: `fix` reports the files it rewrote, `diff` writes a patch, and +/// `strip` writes the stripped source. The flag is global, so asking for it +/// there is a usage error rather than a flag that quietly does nothing. +#[test] +fn explain_is_refused_by_the_commands_that_write_no_report() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("a.rs"), b"let x = 1; // TODO\n").unwrap(); + let refused: [&[&str]; 4] = [ + &["fix", "--explain", "--dry-run"], + &["fix", "--explain"], + &["diff", "--explain"], + &["strip", "--explain", "--language", "rust"], + ]; + for arguments in refused { + let output = run_stdin(directory.path(), arguments, b"let x = 1; // TODO\n"); + assert_eq!( + output.status.code(), + Some(2), + "`ocomment {}` was accepted", + arguments.join(" ") + ); + let error = String::from_utf8_lossy(&output.stderr); + assert!( + error.contains("--explain is only available with `check` and `scan`"), + "`ocomment {}` said:\n{error}", + arguments.join(" ") + ); + assert!( + output.stdout.is_empty(), + "`ocomment {}` wrote a report anyway", + arguments.join(" ") + ); + } + assert_eq!( + fs::read(directory.path().join("a.rs")).unwrap(), + b"let x = 1; // TODO\n", + "a refused run rewrote the file anyway" + ); + // The two commands the flag is for still take it. + for command in ["check", "scan"] { + let output = run(directory.path(), &[command, "--explain"]); + assert!( + output.status.code() != Some(2), + "`ocomment {command} --explain` was refused:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + String::from_utf8_lossy(&output.stdout).contains("a.rs"), + "`ocomment {command} --explain` wrote no report" + ); + } +} + +/// `scan` already lists every comment; `--explain` puts the reason under each +/// of its lines without disturbing the listing itself. +#[test] +fn scan_explain_annotates_every_listed_comment() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("a.py"), + b"#!/usr/bin/env python3\nx = 1 # remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["scan", "a.py", "--explain"]); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let lines: Vec<&str> = stdout.lines().collect(); + assert_eq!( + lines.first().copied(), + Some("a.py:1:1: shebang keep (required source preamble) 0..22: #!/usr/bin/env python3"), + "`scan --explain` changed the listing:\n{stdout}" + ); + assert!( + lines + .get(1) + .is_some_and(|line| line.starts_with(" kept: required source preamble")), + "`scan --explain` did not explain the shebang:\n{stdout}" + ); + assert_eq!( + lines.get(2).copied(), + Some("a.py:2:8: line remove 30..38: # remove"), + "`scan --explain` changed the listing:\n{stdout}" + ); + assert!( + lines.get(3).is_some_and( + |line| line.starts_with(" removed: policy `safe` removes ordinary comments") + ), + "`scan --explain` did not explain the removal:\n{stdout}" + ); + assert_no_debug_leak("human scan --explain output", &stdout); +} + +/// A staged run reads index blobs through a path that carries no policy trace, +/// so it says so rather than printing a listing with every explanation missing. +#[test] +fn explain_is_refused_by_a_staged_run() { + let directory = repository(); + fs::write(directory.path().join("a.rs"), b"let x = 1; // TODO\n").unwrap(); + git(directory.path(), &["add", "a.rs"]); + let output = run(directory.path(), &["check", "--staged", "--explain"]); + assert_eq!(output.status.code(), Some(2)); + let error = String::from_utf8_lossy(&output.stderr); + assert!( + error.contains("--explain is not available with --staged"), + "`check --staged --explain` said:\n{error}" + ); +} + +/// `fix -i` asks a question per comment, so it needs somebody there to answer +/// it. A piped or redirected run would otherwise read the prompt's answer out +/// of whatever the pipe carried — a script's own data — and start writing +/// files from it. The refusal names both ways out and touches nothing. +#[test] +fn fix_interactive_without_a_terminal_refuses_and_writes_nothing() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("sample.rs"); + let before = b"let x = 1; // remove\n"; + fs::write(&path, before).unwrap(); + + let output = run_stdin(directory.path(), &["fix", "-i", "sample.rs"], b"y\n"); + assert_eq!(output.status.code(), Some(2)); + assert_eq!(fs::read(&path).unwrap(), before); + assert_eq!(String::from_utf8(output.stdout).unwrap(), ""); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "ocomment: --interactive needs a terminal; run without -i or use `ocomment diff`\n" + ); +} + +/// The long spelling refuses the same way, so a script that uses it is not +/// told something different from one that uses `-i`. +#[test] +fn fix_interactive_long_spelling_refuses_without_a_terminal() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("sample.rs"), + b"let x = 1; // remove\n", + ) + .unwrap(); + let output = run(directory.path(), &["fix", "--interactive", "sample.rs"]); + assert_eq!(output.status.code(), Some(2)); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "ocomment: --interactive needs a terminal; run without -i or use `ocomment diff`\n" + ); +} + +/// A machine format has no prompt to put a question on and no place to put the +/// answer, so the combination is refused rather than quietly ignoring one of +/// the two flags. It is refused before the terminal is looked at, because the +/// flag combination is wrong however the run was started. +#[test] +fn fix_interactive_refuses_a_machine_format() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("sample.rs"); + let before = b"let x = 1; // remove\n"; + fs::write(&path, before).unwrap(); + + let output = run( + directory.path(), + &["fix", "-i", "--format", "json", "sample.rs"], + ); + assert_eq!(output.status.code(), Some(2)); + assert_eq!(fs::read(&path).unwrap(), before); + assert_eq!( + String::from_utf8(output.stderr).unwrap(), + "ocomment: --interactive is only available with --format human\n" + ); +} + +/// Each of these describes a run that cannot also be interactive: the index +/// carries no working-tree file to show a hunk from, `--dry-run` writes +/// nothing whatever the answers were, and `-q` asks for a run with no +/// commentary at all. Clap refuses the pair at parse time, before any file is +/// read. +#[test] +fn fix_interactive_conflicts_with_the_flags_that_contradict_it() { + let directory = repository(); + let path = directory.path().join("sample.rs"); + let before = b"let x = 1; // remove\n"; + fs::write(&path, before).unwrap(); + git(directory.path(), &["add", "sample.rs"]); + + for conflicting in ["--staged", "--dry-run", "--quiet", "-q"] { + let output = run(directory.path(), &["fix", "-i", conflicting]); + assert_eq!( + output.status.code(), + Some(2), + "`fix -i {conflicting}` was accepted" + ); + let error = String::from_utf8(output.stderr).unwrap(); + assert!( + error.contains("cannot be used with"), + "`fix -i {conflicting}` did not report a conflict:\n{error}" + ); + assert_eq!( + fs::read(&path).unwrap(), + before, + "`fix -i {conflicting}` reached the file" + ); + } +} + +/// The flag is discoverable where the reader looks for it. +#[test] +fn help_documents_the_interactive_fix() { + let directory = tempfile::tempdir().unwrap(); + let fixed = run(directory.path(), &["fix", "--help"]); + assert_eq!(fixed.status.code(), Some(0)); + let help = String::from_utf8(fixed.stdout).unwrap(); + assert!( + help.contains("-i, --interactive"), + "`fix --help` does not document --interactive:\n{help}" + ); +} diff --git a/rust/ocomment/tests/source_guards.rs b/rust/ocomment/tests/source_guards.rs index 77aba32..1a1182d 100644 --- a/rust/ocomment/tests/source_guards.rs +++ b/rust/ocomment/tests/source_guards.rs @@ -9,12 +9,13 @@ use std::{collections::BTreeSet, fs, path::PathBuf}; /// Every source file of the crate, embedded at compile time so the scan does /// not depend on the directory the test runs in. `the_guard_reads_every_source` /// keeps this list equal to what is on disk. -const SOURCES: [(&str, &str); 10] = [ +const SOURCES: [(&str, &str); 11] = [ ("atomic.rs", include_str!("../src/atomic.rs")), ("cli.rs", include_str!("../src/cli.rs")), ("config.rs", include_str!("../src/config.rs")), ("files.rs", include_str!("../src/files.rs")), ("git.rs", include_str!("../src/git.rs")), + ("interactive.rs", include_str!("../src/interactive.rs")), ("lsp.rs", include_str!("../src/lsp.rs")), ("main.rs", include_str!("../src/main.rs")), ("output.rs", include_str!("../src/output.rs")), From 83a5bbdd6cc788356c97c6923f94d32ea87aa868 Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Wed, 26 Aug 2026 07:08:45 +0900 Subject: [PATCH 06/21] feat(ci): pre-commit hooks, composite GitHub Action, SARIF rule catalogue; never walk into .git - .pre-commit-hooks.yaml (ocomment-check / ocomment-fix, language: system) with a drift check that regenerates the files: regex from spec/languages.toml - action.yml: downloads the release archive for the runner, verifies SHA256SUMS and (when gh is present) the build attestation, runs check/diff with github or sarif output, optional code-scanning upload, exit-code/fail-on-findings contract; ci.yml action-smoke job on three OSes with the built binary; rulesets updated - SARIF: tool.driver.version/informationUri/rules with ruleIndex, %SRCROOT% uriBaseId, no ./ prefixes (also for GitHub annotations); tools/validate_schemas.py --sarif; release_manifests.py defaults to P4suta/OComment - safety: .git directories/files are excluded from every walk even when named explicitly; displayed paths and invalid regex patterns are sanitised; --explain is an allow-list (implicit, check, scan); interactive summary reports files scanned - docs/ci.md, README hooks/CI section, releasing notes Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RLSWPMQ9PvtGi3jyBF7bJx --- .github/rulesets/main.json | 3 + .github/workflows/ci.yml | 54 +++++ .pre-commit-hooks.yaml | 22 ++ CHANGELOG.md | 34 ++- CONTRIBUTING.md | 6 + README.md | 38 ++- action.yml | 305 +++++++++++++++++++++++ docs/ci.md | 277 +++++++++++++++++++++ docs/releasing.md | 26 ++ rust/ocomment/src/cli.rs | 15 +- rust/ocomment/src/config.rs | 14 +- rust/ocomment/src/files.rs | 18 ++ rust/ocomment/src/output.rs | 300 +++++++++++++++++++++-- rust/ocomment/tests/cli.rs | 465 +++++++++++++++++++++++++++++++++++- tools/check_hooks.py | 167 +++++++++++++ tools/release_manifests.py | 5 +- tools/validate_schemas.py | 226 +++++++++++++++++- 17 files changed, 1937 insertions(+), 38 deletions(-) create mode 100644 .pre-commit-hooks.yaml create mode 100644 action.yml create mode 100644 docs/ci.md create mode 100644 tools/check_hooks.py diff --git a/.github/rulesets/main.json b/.github/rulesets/main.json index affa17d..37ca1cc 100644 --- a/.github/rulesets/main.json +++ b/.github/rulesets/main.json @@ -36,6 +36,9 @@ {"context": "host-smoke (ubuntu-latest)"}, {"context": "host-smoke (macos-15)"}, {"context": "host-smoke (windows-2025)"}, + {"context": "action-smoke (ubuntu-latest)"}, + {"context": "action-smoke (macos-15)"}, + {"context": "action-smoke (windows-2025)"}, {"context": "Analyze (actions)"}, {"context": "Analyze (python)"}, {"context": "Analyze (rust)"} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a64e44f..af8fd72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: - run: cargo clippy --manifest-path rust/Cargo.toml --workspace --all-targets --locked -- -D warnings - run: cargo test --manifest-path rust/Cargo.toml --workspace --all-targets --locked - run: python3 tools/check_embedded_specs.py + - run: python3 tools/check_hooks.py - run: python3 -m pip install --disable-pip-version-check jsonschema==4.25.1 - run: cargo build --manifest-path rust/Cargo.toml --locked -p ocomment - run: python3 tools/validate_schemas.py @@ -84,3 +85,56 @@ jobs: if: runner.os == 'Windows' shell: pwsh run: '& rust/target/release/ocomment.exe --version' + + action-smoke: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-15, windows-2025] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable + - run: cargo build --manifest-path rust/Cargo.toml --locked -p ocomment + - name: Create the action fixture + shell: bash + run: | + set -euo pipefail + mkdir -p action-fixture + printf 'fn main() {\n let value = 1; // removable\n}\n' >action-fixture/sample.rs + printf 'def sample():\n return 1 # removable\n' >action-fixture/sample.py + - name: Run the composite action against the fixture + id: smoke + uses: ./ + with: + command: check + paths: action-fixture + format: sarif + sarif-file: ocomment.sarif + upload-sarif: "false" + fail-on-findings: "false" + verify-attestation: "false" + binary-path: rust/target/debug/ocomment + - name: Validate the SARIF the action produced + shell: bash + env: + SMOKE_EXIT_CODE: ${{ steps.smoke.outputs.exit-code }} + SMOKE_SARIF_FILE: ${{ steps.smoke.outputs.sarif-file }} + SMOKE_VERSION: ${{ steps.smoke.outputs.version }} + run: | + set -euo pipefail + if [ "${SMOKE_EXIT_CODE}" != "1" ]; then + echo "::error::the fixture has removable comments, so the action should report exit code 1, not ${SMOKE_EXIT_CODE}" + exit 1 + fi + if [ -z "${SMOKE_VERSION}" ]; then + echo "::error::the action reported no version" + exit 1 + fi + python=python3 + command -v python3 >/dev/null 2>&1 || python=python + "${python}" tools/validate_schemas.py --sarif "${SMOKE_SARIF_FILE}" | tee sarif-report.txt + grep -q 'with 2 ocomment results' sarif-report.txt diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..2cb49ef --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,22 @@ +# Hook definitions consumed by pre-commit when this repository is used as a +# `repo:` entry. The `files:` patterns are generated from spec/languages.toml +# and are enforced by tools/check_hooks.py, which CI runs on every change. +# +# `language: system` requires `ocomment` to already be on PATH: pre-commit's +# `language: rust` runs `cargo install --path .` at the checkout root, and this +# repository's manifest lives in rust/, so it cannot build these hooks. +- id: ocomment-check + name: ocomment check + description: 'Report removable comments in the staged source files; exit 1 blocks the commit, exit 2 signals an invalid source, configuration, plugin, or I/O failure.' + entry: ocomment check + language: system + types: [text] + files: '(?i)\.(bash|c|cc|cjs|cpp|css|cts|cu|cxx|go|h|hpp|htm|html|java|js|json5|jsonc|jsx|kt|kts|m|mjs|ml|mli|mm|mts|py|pyi|pyw|rs|sh|sql|ts|tsx|xhtml|zsh)$' +- id: ocomment-fix + name: ocomment fix + description: 'Remove comments from the staged source files in place; exit 1 from the following ocomment-check run, or a file pre-commit sees modified, blocks the commit until the result is reviewed and staged.' + entry: ocomment fix + language: system + types: [text] + require_serial: true + files: '(?i)\.(bash|c|cc|cjs|cpp|css|cts|cu|cxx|go|h|hpp|htm|html|java|js|json5|jsonc|jsx|kt|kts|m|mjs|ml|mli|mm|mts|py|pyi|pyw|rs|sh|sql|ts|tsx|xhtml|zsh)$' diff --git a/CHANGELOG.md b/CHANGELOG.md index ad5dca5..e977539 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,8 +47,8 @@ All notable changes to OComment will be documented here. The project follows `[languages.]` table, the `[[overrides]]` entry whose globs matched, the command-line flag, or the built-in default. A comment a built-in rule decided is left with the flag that would overrule it. The machine formats refuse the - flag rather than ignoring it, and so do `fix`, `diff`, and `strip`, which - write no report for it to annotate. + flag rather than ignoring it, and so does every command that writes no report + of comments for it to annotate. ### Changed @@ -75,3 +75,33 @@ All notable changes to OComment will be documented here. The project follows - Failures say what to do next: how to add `version = 1`, which flag forces a language, how to clear a stale `.git/index.lock`, and which missing tool `ocomment doctor` diagnoses. +- `--format sarif` describes the rules it reports. `tool.driver` names the + version that produced the run and carries a `rules` array: one entry for + every comment kind — a title, a sentence, a link, and a default level — plus + an entry for each scan diagnostic, skipped file, and unreadable file the run + actually met. Every result points at its own entry through `ruleIndex`. A + code-scanning UI titles a finding, describes it, and links out of it through + that entry, so a finding used to arrive as a bare rule id and nothing else. + +### Fixed + +- A walk never descends into `.git`, whatever lifted the hidden-file rule. + Naming a directory does lift it, and so does `files.hidden`, so `ocomment fix + .` in a fresh repository used to rewrite the sample hooks git had just + written into `.git/hooks`. The exclusion covers the `.git` *file* a submodule + or a linked worktree keeps in place of the directory. A path named inside + `.git` is still a request and is still answered. +- SARIF and GitHub annotations spell a reported path the way the checkout + spells it: forward slashes on every platform, and none of the `.` segments a + typed target leaves behind — `ocomment check sub/./doc.rs` reported + `sub/./doc.rs`, which matches no file in any repository, so the annotation + landed on nothing and the SARIF result located nothing. A relative + `artifactLocation` now also carries `uriBaseId: "%SRCROOT%"`; a SARIF reader + given no base id has nothing to resolve the path against. An absolute path, a + path that climbs out of the tree through `..`, and the `` pseudo-path + carry no base id, because none of them is under the source root. +- `tools/release_manifests.py` defaults `--repository` to `P4suta/OComment`. + The release workflow passes `$GITHUB_REPOSITORY`, so the old default only + ever reached someone generating the definitions by hand — and pointed the + Homebrew formula, the Scoop manifest, and the WinGet manifest it wrote at a + repository that is not this one. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0636de5..a7c4a58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,7 @@ cargo test --manifest-path rust/Cargo.toml --workspace --all-targets --locked opam exec -- dune runtest --root ocaml opam exec -- ./tools/differential.sh python3 tools/check_embedded_specs.py +python3 tools/check_hooks.py python3 tools/validate_schemas.py ./tools/package-list.sh actionlint @@ -76,6 +77,11 @@ JSONL, SARIF, and GitHub output. All user-facing text is English. - Add tests for observable behavior and update user-facing documentation. - Regenerate checked-in schemas, WIT, man pages, or completions when their source changes; `tools/check_embedded_specs.py` checks shared embedded assets. +- Adding a language to `spec/languages.toml` also changes the published + pre-commit hooks; `tools/check_hooks.py --print-pattern` regenerates the + `files:` regex that `.pre-commit-hooks.yaml` must carry. +- A new CI job must be added to `.github/rulesets/main.json` in the same change, + and every `uses:` must be SHA-pinned with a version comment. - Do not include build output, credentials, or unrelated formatting changes. The repository uses squash merges. By submitting a contribution, you agree that diff --git a/README.md b/README.md index acdf317..c73b161 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,8 @@ repository above it — however deep in the tree the command is run from. Run `ocomment init config` for the complete default file or `ocomment config schema` for its JSON Schema. See [configuration](docs/configuration.md), -[editor/LSP setup](docs/editors.md), and [plugins](docs/plugins.md). +[editor/LSP setup](docs/editors.md), [plugins](docs/plugins.md), and +[hooks and CI](docs/ci.md). ## Partially staged changes @@ -142,6 +143,41 @@ The generated hook deliberately does not use Lefthook `stage_fixed`, because that setting would add the complete working-tree file and destroy partial staging. +## Hooks and CI + +`.pre-commit-hooks.yaml` publishes `ocomment-check` and `ocomment-fix` for +[pre-commit](https://pre-commit.com). The hooks are `language: system`, so +install the CLI first, then point a `.pre-commit-config.yaml` at this +repository: + +```yaml +repos: + - repo: https://github.com/P4suta/OComment + rev: v0.1.0 + hooks: + - id: ocomment-check +``` + +`ocomment-check` exits 1 and blocks the commit while a staged file still has a +removable comment. `args: ["--staged"]` judges the index blobs rather than the +working tree, which is what a partially staged file needs. + +`action.yml` is a composite GitHub Action. It downloads the release archive for +the runner, verifies its SHA-256 and its build-provenance attestation, and +annotates the pull request: + +```yaml + - uses: P4suta/OComment@v0.1.0 + with: + paths: src tests +``` + +`format: sarif` with `upload-sarif: "true"` sends the findings to code scanning +instead, and `fail-on-findings: "false"` leaves the verdict to a later step +reading the `exit-code` output. [docs/ci.md](docs/ci.md) documents every input +and output, the `--staged` caveats, and how to run the action where no release +archive is published. + ## Library ```rust diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..f9b0c5a --- /dev/null +++ b/action.yml @@ -0,0 +1,305 @@ +name: OComment +description: Report or diff removable comments with the OComment CLI, with optional SARIF upload to code scanning. +author: P4suta + +branding: + icon: scissors + color: purple + +inputs: + version: + description: Release tag to download, such as v0.1.0. Empty resolves to the tag this action was referenced by when that looks like a version, and otherwise to the latest release. + required: false + default: "" + args: + description: Extra arguments appended to the command, split on whitespace. Quoting is not interpreted. + required: false + default: "" + policy: + description: Value for --policy (safe, legal, all). Empty leaves the configured policy alone. + required: false + default: "" + format: + description: Value for --format (human, json, jsonl, sarif, github). + required: false + default: github + paths: + description: Files or directories to process, split on whitespace. Empty checks the working directory. + required: false + default: "" + command: + description: Which OComment command to run, check or diff. + required: false + default: check + fail-on-findings: + description: Fail the step when OComment exits 1 because removable comments exist. Exit 2 always fails. + required: false + default: "true" + upload-sarif: + description: Upload the SARIF file to GitHub code scanning. Requires format sarif and the security-events write permission. + required: false + default: "false" + sarif-file: + description: Where to write SARIF output when format is sarif. + required: false + default: ocomment.sarif + verify-attestation: + description: Verify the downloaded archive's build provenance with `gh attestation verify` when the gh CLI is available. + required: false + default: "true" + binary-path: + description: Use this already-built ocomment binary and download nothing. A missing path is retried with an .exe suffix. + required: false + default: "" + working-directory: + description: Directory the command runs in. Relative paths and SARIF output resolve against it. + required: false + default: "." + token: + description: Token used to resolve the latest release and verify attestations. + required: false + default: ${{ github.token }} + +outputs: + exit-code: + description: Exit code of the OComment run; 0 clean, 1 removable comments, 2 failure. + value: ${{ steps.run.outputs.exit-code }} + version: + description: Release tag that was downloaded, or the version the supplied binary reported. + value: ${{ steps.resolve.outputs.version }} + sarif-file: + description: Absolute path of the SARIF file, empty when format is not sarif. + value: ${{ steps.run.outputs.sarif-file }} + +runs: + using: composite + steps: + - name: Resolve the OComment version + id: resolve + shell: bash + env: + INPUT_BINARY_PATH: ${{ inputs.binary-path }} + INPUT_VERSION: ${{ inputs.version }} + ACTION_REF: ${{ github.action_ref }} + OCOMMENT_REPO: P4suta/OComment + GH_TOKEN: ${{ inputs.token }} + run: | + set -euo pipefail + + if [ -n "${INPUT_BINARY_PATH}" ]; then + binary="${INPUT_BINARY_PATH}" + if [ ! -f "${binary}" ] && [ -f "${binary}.exe" ]; then + binary="${binary}.exe" + fi + if [ ! -f "${binary}" ]; then + echo "::error::binary-path does not name a file: ${INPUT_BINARY_PATH}" + exit 2 + fi + binary="$(cd "$(dirname "${binary}")" && pwd)/$(basename "${binary}")" + chmod +x "${binary}" 2>/dev/null || true + version="$("${binary}" --version | awk 'NR == 1 {print $NF}')" + echo "Using the supplied binary ${binary} (ocomment ${version})" + { + echo "binary=${binary}" + echo "version=${version}" + echo "download=false" + } >>"${GITHUB_OUTPUT}" + exit 0 + fi + + version="${INPUT_VERSION}" + if [ -z "${version}" ] && [[ "${ACTION_REF}" == v* ]]; then + version="${ACTION_REF}" + fi + if [ -z "${version}" ]; then + version="$(gh release view --repo "${OCOMMENT_REPO}" --json tagName --jq .tagName)" + fi + case "${version}" in + v*) ;; + *) version="v${version}" ;; + esac + echo "Resolved OComment ${version}" + { + echo "binary=ocomment" + echo "version=${version}" + echo "download=true" + } >>"${GITHUB_OUTPUT}" + + - name: Download the OComment release archive + id: download + if: steps.resolve.outputs.download == 'true' + shell: bash + env: + OCOMMENT_VERSION: ${{ steps.resolve.outputs.version }} + VERIFY_ATTESTATION: ${{ inputs.verify-attestation }} + RUNNER_OS_NAME: ${{ runner.os }} + RUNNER_ARCH_NAME: ${{ runner.arch }} + OCOMMENT_REPO: P4suta/OComment + GH_TOKEN: ${{ inputs.token }} + run: | + set -euo pipefail + + case "${RUNNER_OS_NAME}/${RUNNER_ARCH_NAME}" in + Linux/X64) target=x86_64-unknown-linux-musl; extension=tar.gz ;; + Linux/ARM64) target=aarch64-unknown-linux-musl; extension=tar.gz ;; + macOS/X64) target=x86_64-apple-darwin; extension=tar.gz ;; + macOS/ARM64) target=aarch64-apple-darwin; extension=tar.gz ;; + Windows/X64) target=x86_64-pc-windows-msvc; extension=zip ;; + *) + echo "::error::no OComment release archive for ${RUNNER_OS_NAME}/${RUNNER_ARCH_NAME}; build the CLI and pass binary-path instead" + exit 2 + ;; + esac + + archive="ocomment-${target}.${extension}" + base="https://github.com/${OCOMMENT_REPO}/releases/download/${OCOMMENT_VERSION}" + install_dir="${RUNNER_TEMP}/ocomment-${OCOMMENT_VERSION}" + mkdir -p "${install_dir}" + cd "${install_dir}" + + curl --fail --silent --show-error --location --retry 3 --retry-all-errors \ + --output "${archive}" "${base}/${archive}" + curl --fail --silent --show-error --location --retry 3 --retry-all-errors \ + --output SHA256SUMS "${base}/SHA256SUMS" + + expected="$(awk -v name="${archive}" '$2 == name || $2 == "*" name {print $1}' SHA256SUMS)" + if [ -z "${expected}" ]; then + echo "::error::${archive} is not listed in the release SHA256SUMS" + exit 2 + fi + if command -v sha256sum >/dev/null 2>&1; then + actual="$(sha256sum "${archive}" | awk '{print $1}')" + else + actual="$(shasum -a 256 "${archive}" | awk '{print $1}')" + fi + if [ "${expected}" != "${actual}" ]; then + echo "::error::SHA-256 mismatch for ${archive}: expected ${expected}, got ${actual}" + exit 2 + fi + echo "Verified ${archive} against the release SHA256SUMS" + + if [ "${VERIFY_ATTESTATION}" = "true" ]; then + if command -v gh >/dev/null 2>&1; then + gh attestation verify "${archive}" --repo "${OCOMMENT_REPO}" + else + echo "::warning::the gh CLI is not installed on this runner; skipping attestation verification" + fi + fi + + if [ "${extension}" = "zip" ]; then + unzip -q -o "${archive}" + else + tar -xzf "${archive}" + fi + + binary_dir="${install_dir}/ocomment-${target}" + binary="${binary_dir}/ocomment" + if [ ! -f "${binary}" ] && [ -f "${binary}.exe" ]; then + binary="${binary}.exe" + fi + if [ ! -f "${binary}" ]; then + echo "::error::${archive} does not contain ocomment-${target}/ocomment" + exit 2 + fi + chmod +x "${binary}" 2>/dev/null || true + echo "${binary_dir}" >>"${GITHUB_PATH}" + echo "binary=${binary}" >>"${GITHUB_OUTPUT}" + + - name: Run OComment + id: run + shell: bash + env: + OCOMMENT_BIN: ${{ steps.download.outputs.binary || steps.resolve.outputs.binary }} + INPUT_COMMAND: ${{ inputs.command }} + INPUT_FORMAT: ${{ inputs.format }} + INPUT_POLICY: ${{ inputs.policy }} + INPUT_PATHS: ${{ inputs.paths }} + INPUT_ARGS: ${{ inputs.args }} + INPUT_SARIF_FILE: ${{ inputs.sarif-file }} + INPUT_UPLOAD_SARIF: ${{ inputs.upload-sarif }} + INPUT_WORKING_DIRECTORY: ${{ inputs.working-directory }} + run: | + set -euo pipefail + + case "${INPUT_COMMAND}" in + check|diff) ;; + *) + echo "::error::command must be check or diff, got ${INPUT_COMMAND}" + exit 2 + ;; + esac + if [ "${INPUT_UPLOAD_SARIF}" = "true" ] && [ "${INPUT_FORMAT}" != "sarif" ]; then + echo "::error::upload-sarif needs format sarif, got ${INPUT_FORMAT}" + exit 2 + fi + cd "${INPUT_WORKING_DIRECTORY}" + + set -f + # shellcheck disable=SC2206 # paths and args are documented as whitespace-separated. + arguments=("${INPUT_COMMAND}" ${INPUT_PATHS}) + arguments+=(--format "${INPUT_FORMAT}") + if [ -n "${INPUT_POLICY}" ]; then + arguments+=(--policy "${INPUT_POLICY}") + fi + # shellcheck disable=SC2206 # paths and args are documented as whitespace-separated. + arguments+=(${INPUT_ARGS}) + set +f + + sarif_file="" + if [ "${INPUT_FORMAT}" = "sarif" ]; then + mkdir -p "$(dirname "${INPUT_SARIF_FILE}")" + : >"${INPUT_SARIF_FILE}" + sarif_file="$(cd "$(dirname "${INPUT_SARIF_FILE}")" && pwd)/$(basename "${INPUT_SARIF_FILE}")" + fi + + printf 'Running: ocomment' + printf ' %s' "${arguments[@]}" + printf '\n' + + set +e + if [ -n "${sarif_file}" ]; then + "${OCOMMENT_BIN}" "${arguments[@]}" >"${sarif_file}" + else + "${OCOMMENT_BIN}" "${arguments[@]}" + fi + code=$? + set -e + + echo "ocomment exited ${code}" + { + echo "exit-code=${code}" + echo "sarif-file=${sarif_file}" + } >>"${GITHUB_OUTPUT}" + + - name: Upload SARIF results + if: ${{ inputs.upload-sarif == 'true' && steps.run.outputs.sarif-file != '' }} + uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + with: + sarif_file: ${{ steps.run.outputs.sarif-file }} + category: ocomment + + - name: Report the OComment verdict + shell: bash + env: + EXIT_CODE: ${{ steps.run.outputs.exit-code }} + FAIL_ON_FINDINGS: ${{ inputs.fail-on-findings }} + run: | + set -euo pipefail + + code="${EXIT_CODE:-2}" + case "${code}" in + 0) + echo "OComment found no removable comments." + ;; + 1) + if [ "${FAIL_ON_FINDINGS}" = "true" ]; then + echo "::error::OComment reported removable comments (exit 1)." + exit 1 + fi + echo "::notice::OComment reported removable comments (exit 1); fail-on-findings is false." + ;; + *) + echo "::error::OComment failed with exit code ${code}." + exit "${code}" + ;; + esac diff --git a/docs/ci.md b/docs/ci.md new file mode 100644 index 0000000..5240e38 --- /dev/null +++ b/docs/ci.md @@ -0,0 +1,277 @@ +# Hooks and CI + +OComment ships two integrations: a [pre-commit](https://pre-commit.com) hook +manifest at `.pre-commit-hooks.yaml`, and a composite GitHub Action at +`action.yml`. Both drive the same CLI and the same exit codes: `0` clean, `1` +removable comments exist, `2` an invalid source, configuration, plugin, or I/O +failure. + +## pre-commit + +### Install the CLI first + +The hooks declare `language: system`, so `ocomment` must already be on `PATH` +when pre-commit runs them. pre-commit's `language: rust` runs +`cargo install --path .` at the checkout root, and this repository's manifest +lives in `rust/`, so it cannot build these hooks. Install the CLI once per +machine and CI image: + +```sh +cargo install ocomment --locked +``` + +A future release may publish a wheel so `language: python` can install the +binary itself. Until then, a missing `ocomment` fails the hook with a "command +not found" error rather than silently passing. + +### Recommended configuration + +```yaml +repos: + - repo: https://github.com/P4suta/OComment + rev: v0.1.0 + hooks: + - id: ocomment-check +``` + +`ocomment-check` reports removable comments in the staged source files and +exits 1, which blocks the commit and leaves the fix to you. That is the safe +default: nothing is rewritten behind your back. + +To rewrite instead of reporting, use `ocomment-fix`. Run it *before* +`ocomment-check` so the check confirms the result: + +```yaml +repos: + - repo: https://github.com/P4suta/OComment + rev: v0.1.0 + hooks: + - id: ocomment-fix + - id: ocomment-check +``` + +Both hooks accept the full CLI surface through `args`, for example +`args: ["--policy", "legal"]` or `args: ["--config", "ci/.ocomment.toml"]`. + +### Judging the commit rather than the disk + +pre-commit passes the staged file names to the hook and stashes unstaged +changes before running it, so by default OComment reads the working tree that +pre-commit has already reduced to the staged content. Add `--staged` to read +the Git index blobs directly — the exact bytes the commit will contain: + +```yaml + - id: ocomment-check + args: ["--staged"] +``` + +For a partially staged file the difference is visible: the working tree shows +every comment, the index shows only the ones being committed. + +```console +$ ocomment check a.rs +a.rs:2:16: removable line comment: // staged comment +a.rs:3:16: removable line comment: // unstaged comment +Found 2 removable comments in 1 file (1 file scanned). Run `ocomment fix` to remove them. + +$ ocomment check --staged a.rs +a.rs:2:16: removable line comment: // staged comment +Found 1 removable comment in 1 file (1 file scanned). Run `ocomment fix` to remove it. +``` + +Two caveats come with `--staged`, and both are worth knowing before you enable +it. + +**`fix --staged` rewrites the index and the working tree together, so +pre-commit does not notice.** pre-commit decides that "files were modified by +this hook" by comparing the unstaged diff before and after the hook. After +pre-commit's stash the working tree already equals the index, and +`ocomment fix --staged` moves both sides by the same edits, so the unstaged +diff is empty both before and after: + +```console +$ git status --short +M a.rs # staged, working tree clean +``` + +The detection therefore does not fire, and the commit proceeds with the +removals already staged. If you want the commit stopped so you can look at the +result, keep `ocomment-fix` without `--staged` — that rewrites only the working +tree, leaves an unstaged diff, and pre-commit fails the commit — or follow it +with `ocomment-check --staged`. + +Outside pre-commit, where a file really is partially staged, `fix --staged` +refuses rather than guessing: + +```console +$ ocomment fix --staged a.rs +ocomment: unstaged changes in a.rs make the staged fix ambiguous; no files were +modified (use --index-only): edit context does not have one unique working-tree +mapping +``` + +**`--staged` sees nothing outside the `pre-commit` stage.** Under +`pre-commit run --all-files`, or in a `pre-push` or `manual` stage, there is no +staged change set, so the run scans zero files and exits 0: + +```console +$ ocomment check --staged +No removable comments in 0 files. +``` + +That is a hook which always passes, not a hook which found nothing. Use a +separate entry without `--staged` for those stages, or gate the `--staged` +entry with `stages: [pre-commit]`. + +### Keeping the hook manifest honest + +The `files:` pattern in `.pre-commit-hooks.yaml` is generated from the +extensions in `spec/languages.toml`, so a new language cannot leave the hooks +scanning the old file set. `tools/check_hooks.py` regenerates the pattern and +fails on any drift; it also rejects a manifest key pre-commit does not define +and a hook missing `id`, `name`, `entry`, or `language`, because pre-commit +itself only complains when a consumer runs the hook. CI runs it next to +`tools/check_embedded_specs.py`. + +```sh +python3 tools/check_hooks.py # fail on drift +python3 tools/check_hooks.py --print-pattern # the regex the hooks must carry +``` + +## GitHub Action + +`action.yml` at the repository root is a composite action. It resolves a +release, downloads the archive for the runner, verifies its SHA-256 and its +build provenance, runs `ocomment check` or `ocomment diff`, and turns the exit +code into a verdict. + +### Annotate a pull request + +`format: github` is the default and writes `::notice` annotations that GitHub +renders on the changed lines. + +```yaml +name: Comments +on: [pull_request] + +permissions: + contents: read + +jobs: + ocomment: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: P4suta/OComment@v0.1.0 + with: + paths: src tests +``` + +### Upload SARIF to code scanning + +```yaml +jobs: + ocomment: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write # Upload the SARIF file to code scanning. + steps: + - uses: actions/checkout@v7 + - uses: P4suta/OComment@v0.1.0 + with: + format: sarif + upload-sarif: "true" + fail-on-findings: "false" # Let the code-scanning alerts carry the result. +``` + +`upload-sarif: "true"` requires `format: sarif`; any other format is a usage +error rather than a silent skip. The SARIF is uploaded under the `ocomment` +category, so it does not collide with other tools' results. + +### Inputs + +| Input | Default | Meaning | +| --- | --- | --- | +| `version` | `""` | Release tag to download. Empty uses the tag the action was referenced by when that looks like a version, and otherwise the latest release. | +| `command` | `check` | `check` or `diff`. | +| `paths` | `""` | Files or directories, split on whitespace. Empty processes the working directory. | +| `policy` | `""` | Value for `--policy`. Empty leaves the configured policy alone. | +| `format` | `github` | Value for `--format`. | +| `args` | `""` | Extra arguments, split on whitespace. | +| `fail-on-findings` | `"true"` | Fail the step on exit 1. Exit 2 always fails. | +| `upload-sarif` | `"false"` | Upload the SARIF file to code scanning. | +| `sarif-file` | `ocomment.sarif` | Where SARIF output is written. | +| `verify-attestation` | `"true"` | Run `gh attestation verify` on the archive when `gh` is available. | +| `binary-path` | `""` | Use an already-built binary and download nothing. | +| `working-directory` | `.` | Directory the command runs in. | +| `token` | `${{ github.token }}` | Used to resolve the latest release and verify attestations. | + +`paths` and `args` are split on whitespace with globbing disabled; quoting +inside them is not interpreted, so a path containing a space needs a separate +run or a `--config` file. + +### Outputs + +| Output | Meaning | +| --- | --- | +| `exit-code` | `0` clean, `1` removable comments, `2` failure. | +| `version` | Release tag downloaded, or the version the supplied binary reported. | +| `sarif-file` | Absolute path of the SARIF file, empty when `format` is not `sarif`. | + +`fail-on-findings: "false"` keeps the step green on exit 1 so a later step can +branch on `exit-code`: + +```yaml + - id: comments + uses: P4suta/OComment@v0.1.0 + with: + fail-on-findings: "false" + - if: steps.comments.outputs.exit-code == '1' + run: echo "Removable comments are present but not blocking." +``` + +### What the action verifies + +Every downloaded archive is checked against the release `SHA256SUMS` before it +is unpacked, and the run stops with exit 2 on a mismatch or on an archive that +the checksum file does not list. With `verify-attestation: "true"` — the +default — the archive is also checked against its GitHub build-provenance +attestation with `gh attestation verify --repo P4suta/OComment`. Runners +without the `gh` CLI log a warning and continue; runners with it fail the step +when the attestation does not verify. + +Runner platforms map to the published targets as follows. Linux uses the +statically linked musl archives, so no glibc version is required. + +| Runner | Target | Archive | +| --- | --- | --- | +| Linux x64 | `x86_64-unknown-linux-musl` | `.tar.gz` | +| Linux arm64 | `aarch64-unknown-linux-musl` | `.tar.gz` | +| macOS x64 | `x86_64-apple-darwin` | `.tar.gz` | +| macOS arm64 | `aarch64-apple-darwin` | `.tar.gz` | +| Windows x64 | `x86_64-pc-windows-msvc` | `.zip` | + +Any other combination fails with exit 2 and points at `binary-path`. + +### Runners without a published archive + +`binary-path` skips resolution and download entirely and uses a binary you +already have. A missing path is retried with an `.exe` suffix, so one value +works across the runner matrix. This is how the repository's own +`action-smoke` job tests the action against a freshly built CLI: + +```yaml + - run: cargo build --manifest-path rust/Cargo.toml --locked -p ocomment + - uses: ./ + with: + binary-path: rust/target/debug/ocomment + paths: action-fixture +``` + +### Pinning + +Version tags are immutable under the repository's release-tag ruleset, so +`P4suta/OComment@v0.1.0` is a stable reference and there is no moving `v0` tag +to follow. Pin to a full version, or to a commit SHA with a version comment if +your policy requires it. diff --git a/docs/releasing.md b/docs/releasing.md index b40a4a3..6eed140 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -31,3 +31,29 @@ bucket, and WinGet repository; they can also be installed directly while an upstream submission is pending. The CLI crate contains explicit `cargo-binstall` URL, archive-format, and in-archive binary metadata for the same target-qualified archives. + +## The published GitHub Action + +`action.yml` at the repository root is released with the source, so +`P4suta/OComment@vMAJOR.MINOR.PATCH` resolves as soon as the tag exists; no +extra publishing step is needed for it to work in a workflow. + +Listing it on the GitHub Marketplace is separate and manual. The first time, +open the release in the GitHub UI and tick **Publish this Action to the GitHub +Marketplace** before publishing the release; the checkbox appears only when +`action.yml` is present at the repository root with a `name`, `description`, +and `branding` block. Later releases inherit the listing, and the Marketplace +version list follows the tags. + +Recommend full-version pins such as `P4suta/OComment@v0.1.0` in every example. +The release-tag ruleset forbids deleting or force-moving `v*`, so a published +tag never changes underneath a workflow, and there is deliberately no moving +`v0` or `v0.1` tag to maintain. A release that changes the action's inputs, +outputs, or verdict rules is therefore a version bump like any other, and +`docs/ci.md` documents the surface those pins are buying. + +The action downloads `ocomment-.tar.gz` (or the Windows `.zip`), the +combined `SHA256SUMS`, and — when `gh` is on the runner — the build-provenance +attestation. Renaming a release asset, dropping a musl target, or changing the +`ocomment-/` leading directory breaks every pinned workflow, so treat +the archive layout as part of the released contract. diff --git a/rust/ocomment/src/cli.rs b/rust/ocomment/src/cli.rs index 6ea2ea9..8414825 100644 --- a/rust/ocomment/src/cli.rs +++ b/rust/ocomment/src/cli.rs @@ -470,14 +470,17 @@ pub fn run() -> Result { if common.output.explain && common.output.format != OutputFormat::Human { bail!("--explain is only available with --format human"); } - // The flag annotates a report of comments, and only `check` and `scan` - // write one: `fix` reports the files it rewrote, `diff` writes a patch, - // and `strip` writes the stripped source. `--explain` is global, so the - // combination is refused rather than quietly doing nothing. + // The flag annotates a report of comments, and only `check`, `scan` and + // the implicit command write one: `fix` reports the files it rewrote, + // `diff` writes a patch, `strip` writes the stripped source, and the rest + // of the commands answer a question that is not about comments at all. + // `--explain` is global, so it is named as an allow-list — a command added + // later has to opt in — and everything else is refused rather than + // quietly doing nothing. if common.output.explain - && matches!( + && !matches!( cli.command, - Some(Command::Fix(_) | Command::Diff(_) | Command::Strip) + None | Some(Command::Check(_) | Command::Scan(_)) ) { bail!("--explain is only available with `check` and `scan`"); diff --git a/rust/ocomment/src/config.rs b/rust/ocomment/src/config.rs index 37a6327..415ec19 100644 --- a/rust/ocomment/src/config.rs +++ b/rust/ocomment/src/config.rs @@ -745,8 +745,18 @@ fn validate_policy_regexes(config: &Config) -> Result<()> { .flat_map(|item| item.keep_regex.iter().chain(&item.remove_regex)), ); for pattern in patterns { - regex::bytes::Regex::new(pattern) - .with_context(|| format!("invalid comment policy regex `{pattern}`"))?; + regex::bytes::Regex::new(pattern).map_err(|error| { + // Both halves of this line came out of a file in the project: the + // pattern the caller wrote, and a parse error that quotes that + // same pattern back with a caret under it. Neither may reach a + // terminal verbatim, so both are folded onto one control-free + // line the way a comment preview is. + anyhow!( + "invalid comment policy regex `{}`: {}", + crate::output::sanitize_path(pattern), + crate::output::sanitize_path(&error.to_string()) + ) + })?; } Ok(()) } diff --git a/rust/ocomment/src/files.rs b/rust/ocomment/src/files.rs index 83d3439..63345d1 100644 --- a/rust/ocomment/src/files.rs +++ b/rust/ocomment/src/files.rs @@ -136,6 +136,20 @@ pub fn stdin_source( /// they did not mean to rewrite — into the run. pub const DEFAULT_TARGET: &str = "."; +/// The one name a walk never offers, whatever else was asked for. +/// +/// `.git` is git's own storage rather than source, and `git` itself never +/// treats it as a candidate for anything. Neither may a tool that rewrites +/// files in place: `ocomment fix .` in a fresh repository would otherwise +/// rewrite every sample hook git had just written into `.git/hooks`. Naming +/// a directory lifts the hidden-file rule and so does `files.hidden`, so the +/// exclusion cannot hang off either of them. +/// +/// A submodule or a linked worktree keeps its `.git` as a *file* pointing at +/// the storage instead of holding it, which is why the name is matched rather +/// than the file type. +const GIT_DIRECTORY: &str = ".git"; + pub fn discover( paths: &[PathBuf], resolved: &ResolvedConfig, @@ -215,6 +229,10 @@ fn discover_with_scope( if ignore { builder.add_custom_ignore_filename(".ocommentignore"); } + // The filter is never asked about the walk root, so a caller who + // names a path inside `.git` — or `.git` itself — is still + // answered; only what a walk *wanders* into is excluded. + builder.filter_entry(|entry| entry.file_name() != GIT_DIRECTORY); for entry in builder.build() { match entry { Ok(entry) if entry.file_type().is_some_and(|kind| kind.is_file()) => { diff --git a/rust/ocomment/src/output.rs b/rust/ocomment/src/output.rs index 6eda74d..93ed409 100644 --- a/rust/ocomment/src/output.rs +++ b/rust/ocomment/src/output.rs @@ -1,6 +1,6 @@ use crate::{ config::PolicyTrace, - files::{NO_LANGUAGE, SkippedFile}, + files::{NO_LANGUAGE, STDIN_PATH, SkippedFile}, }; use anyhow::Result; use clap::ValueEnum; @@ -14,7 +14,7 @@ use similar::{ChangeTag, TextDiff}; use std::{ collections::BTreeMap, io::{self, BufWriter, Write}, - path::{Path, PathBuf}, + path::{Component, Path, PathBuf}, }; use unicode_width::UnicodeWidthChar; @@ -842,6 +842,11 @@ pub(crate) struct InteractiveOutcome { /// actually asked, and says how many it never got to: measuring the acceptances /// against every comment the run *could* have offered would read as a pile of /// refusals nobody made. +/// +/// Either way the verdict closes on the `(N files scanned)` every other summary +/// ends with. Answering questions about three files says nothing about how many +/// were opened to find them, and that is the number a reader checks a run +/// against. pub(crate) fn interactive_summary(outcome: InteractiveOutcome) -> String { if outcome.offered == 0 { return format!("Nothing to fix in {}.", plural(outcome.scanned, "file")); @@ -853,10 +858,11 @@ pub(crate) fn interactive_summary(outcome: InteractiveOutcome) -> String { format!(" ({} not reviewed)", comments(unreviewed, "")) }; format!( - "Removed {} of {} in {}{tail}.", + "Removed {} of {} in {}{tail} ({} scanned).", outcome.removed, comments(outcome.reviewed, ""), - plural(outcome.changed, "file") + plural(outcome.changed, "file"), + plural(outcome.scanned, "file") ) } @@ -1014,8 +1020,16 @@ pub(crate) fn color(code: &'static str, enabled: bool) -> &'static str { if enabled { code } else { "" } } +/// The path half of a report line, and the hyperlink wrapped around it. +/// +/// A file name is chosen by whoever made the file, so the shown half is +/// untrusted input on its way to a terminal exactly like the preview beside +/// it, and gets `sanitize_path`'s treatment: one line, no control characters, +/// and no width cap, because a path cut to an ellipsis names no file. The +/// link *target* is a URL rather than terminal text and keeps the +/// percent-encoding it has always had. fn display_path(path: &Path, hyperlinks: bool) -> String { - let display = path.display().to_string(); + let display = sanitize_path(&path.display().to_string()); if !hyperlinks { return display; } @@ -1080,13 +1094,155 @@ fn json_file(file: &ProcessedFile) -> JsonFile<'_> { } } +/// Where a SARIF reader is sent to learn what the tool itself is. +const TOOL_INFORMATION_URI: &str = "https://github.com/P4suta/OComment"; + +/// Where a rule about a comment sends a reader asking why that comment is +/// reported — and why the one beside it is not. +const KIND_HELP_URI: &str = "https://github.com/P4suta/OComment#why-was-this-comment-kept"; + +/// The base id a path under the directory the run walked is reported against. +/// SARIF readers, GitHub code scanning among them, resolve `%SRCROOT%` to the +/// root of the checkout. +const SRCROOT: &str = "%SRCROOT%"; + +/// The one sentence every scan diagnostic is described by. The codes are as +/// varied as the languages that raise them, and the result carries the message +/// that says what was actually met. +const DIAGNOSTIC_DESCRIPTION: &str = + "A problem OComment met while scanning the file; the message on the result says what it was."; + +/// The spelling a machine format reports a path under. +/// +/// A SARIF `artifactLocation.uri` and the `file=` of a GitHub annotation are +/// both matched against the paths the repository uses, so a reported path is +/// spelled the way the repository spells it: forward slashes on every +/// platform, and none of the `.` segments a walk root or a typed target leaves +/// behind — `sub/./doc.rs` names a file no checkout has. What a relative path +/// is measured *from* is said separately, by [`artifact_location`]. +fn report_uri(path: &Path) -> String { + let text = path.to_string_lossy().replace('\\', "/"); + let trimmed: Vec<&str> = text.split('/').filter(|segment| *segment != ".").collect(); + if trimmed.is_empty() { + // The path was `.` (or `./`) and naming nothing at all would be worse + // than naming the directory. + return text; + } + trimmed.join("/") +} + +/// The SARIF `artifactLocation` for a reported path. +/// +/// A path under the directory the run started in is reported against +/// `%SRCROOT%`: SARIF resolves a relative URI against a base id, and a reader +/// given none has nothing to resolve it against, so the finding lands on no +/// file. An absolute path is not under the checkout as far as the run can +/// tell, one that climbs out through `..` has left it, and the pseudo-path +/// standard input is reported under is not a file at all — each of those is +/// reported as it stands, with no base id claiming otherwise. +fn artifact_location(path: &Path) -> Value { + let uri = report_uri(path); + if under_source_root(path) { + json!({"uri": uri, "uriBaseId": SRCROOT}) + } else { + json!({"uri": uri}) + } +} + +fn under_source_root(path: &Path) -> bool { + path != Path::new(STDIN_PATH) + && path + .components() + .all(|component| matches!(component, Component::Normal(_) | Component::CurDir)) + && path + .components() + .any(|component| matches!(component, Component::Normal(_))) +} + +/// The rules of one SARIF run, and the index each result points at. +/// +/// A result names its rule twice: by `ruleId`, and by the position of that +/// rule's description in `tool.driver.rules`. A code-scanning UI shows a +/// finding through that description — its title, the sentence under it, and +/// the link it offers — so handing out the id and the index together is what +/// keeps a result from pointing at a description that is not there. +/// +/// Every comment kind is described whether or not the run met one, because the +/// rules a tool reports are also read as the list of what it can find. The +/// rest — a scan diagnostic, a skipped file, a file that could not be read — +/// are described as the run meets them. +struct SarifRules { + entries: Vec, + indices: BTreeMap, +} + +impl SarifRules { + fn new() -> Self { + let mut rules = Self { + entries: Vec::new(), + indices: BTreeMap::new(), + }; + for kind in CommentKind::ALL { + rules.describe( + &format!("removable-{kind}"), + "note", + &format!("Removable {kind} comment"), + &format!( + "A {kind} comment OComment can remove without changing what the file does." + ), + KIND_HELP_URI, + ); + } + rules + } + + /// The index of the rule `id`, describing it first if this run has not + /// reported it before. + fn describe(&mut self, id: &str, level: &str, short: &str, full: &str, help: &str) -> usize { + if let Some(&index) = self.indices.get(id) { + return index; + } + let index = self.entries.len(); + self.entries.push(json!({ + "id": id, + "shortDescription": {"text": short}, + "fullDescription": {"text": full}, + "helpUri": help, + "defaultConfiguration": {"level": level}, + })); + self.indices.insert(id.to_owned(), index); + index + } + + fn kind(&mut self, kind: CommentKind) -> usize { + let id = format!("removable-{kind}"); + *self + .indices + .get(&id) + .expect("every comment kind is described") + } +} + +/// A kebab-cased code read back as the title of a rule: +/// `unterminated-comment` is `Unterminated comment`. +fn sentence_case(code: &str) -> String { + let spelled = code.replace('-', " "); + let mut characters = spelled.chars(); + match characters.next() { + Some(first) => first.to_uppercase().collect::() + characters.as_str(), + None => spelled, + } +} + fn render_sarif( output: &mut impl Write, files: &[ProcessedFile], skipped: &[SkippedFile], ) -> Result<()> { + let mut rules = SarifRules::new(); let mut results = Vec::new(); for file in files { + let location = artifact_location(&file.path); for comment in file .result .report @@ -1097,19 +1253,21 @@ fn render_sarif( let (line, column) = line_column(&file.source, comment.span.start); let (end_line, end_column) = line_column(&file.source, comment.span.end); let kind = comment.kind.as_str(); + let index = rules.kind(comment.kind); results.push(json!({ "ruleId": format!("removable-{kind}"), + "ruleIndex": index, "level": "note", "message": {"text": removable_label(comment.kind)}, "locations": [{"physicalLocation": { - "artifactLocation": {"uri": file.path.to_string_lossy()}, + "artifactLocation": location.clone(), "region": {"startLine": line, "startColumn": column, "endLine": end_line, "endColumn": end_column} }}], "fixes": [{ "description": {"text": "Remove comment with OComment"}, "artifactChanges": [{ - "artifactLocation": {"uri": file.path.to_string_lossy()}, + "artifactLocation": location.clone(), "replacements": [{"deletedRegion": { "startLine": line, "startColumn": column, "endLine": end_line, "endColumn": end_column @@ -1126,12 +1284,20 @@ fn render_sarif( ocomment_core::Severity::Warning => "warning", ocomment_core::Severity::Info | ocomment_core::Severity::Hint => "note", }; + let index = rules.describe( + &diagnostic.code, + level, + &sentence_case(&diagnostic.code), + DIAGNOSTIC_DESCRIPTION, + TOOL_INFORMATION_URI, + ); results.push(json!({ "ruleId": diagnostic.code, + "ruleIndex": index, "level": level, "message": {"text": diagnostic.message}, "locations": [{"physicalLocation": { - "artifactLocation": {"uri": file.path.to_string_lossy()}, + "artifactLocation": location.clone(), "region": {"startLine": line, "startColumn": column, "endLine": end_line, "endColumn": end_column} }}] @@ -1139,19 +1305,41 @@ fn render_sarif( } } for item in skipped { + let (id, level, short, full) = if item.error { + ( + "io-error", + "error", + "File could not be read", + "A file OComment could not read or write; the message on the result carries the operating-system error.", + ) + } else { + ( + "skipped-file", + "note", + "Skipped file", + "A file OComment did not scan; the message on the result says why it was left alone.", + ) + }; + let index = rules.describe(id, level, short, full, TOOL_INFORMATION_URI); results.push(json!({ - "ruleId": if item.error { "io-error" } else { "skipped-file" }, - "level": if item.error { "error" } else { "note" }, + "ruleId": id, + "ruleIndex": index, + "level": level, "message": {"text": item.reason}, "locations": [{"physicalLocation": { - "artifactLocation": {"uri": item.path.to_string_lossy()} + "artifactLocation": artifact_location(&item.path) }}] })); } let sarif = json!({ "version": "2.1.0", "$schema": "https://json.schemastore.org/sarif-2.1.0.json", - "runs": [{"tool": {"driver": {"name": "ocomment", "informationUri": "https://github.com/P4suta/OComment"}}, "results": results}] + "runs": [{"tool": {"driver": { + "name": "ocomment", + "version": env!("CARGO_PKG_VERSION"), + "informationUri": TOOL_INFORMATION_URI, + "rules": rules.entries + }}, "results": results}] }); serde_json::to_writer_pretty(&mut *output, &sarif).map_err(write_error)?; wrote(writeln!(output))?; @@ -1184,7 +1372,7 @@ fn render_github( wrote(writeln!( output, "::notice file={},line={line},col={column}::{}", - github_escape(&file.path.to_string_lossy()), + github_escape(&report_uri(&file.path)), removable_label(comment.kind) ))?; } @@ -1193,7 +1381,7 @@ fn render_github( wrote(writeln!( output, "::error file={},line={line},col={column},title={}::{}", - github_escape(&file.path.to_string_lossy()), + github_escape(&report_uri(&file.path)), github_escape(&diagnostic.code), github_escape(&diagnostic.message) ))?; @@ -1204,7 +1392,7 @@ fn render_github( output, "::{} file={},title={}::{}", if item.error { "error" } else { "notice" }, - github_escape(&item.path.to_string_lossy()), + github_escape(&report_uri(&item.path)), if item.error { "OComment I/O error" } else { @@ -1297,6 +1485,76 @@ fn _span(_: ByteSpan) -> Value { mod tests { use super::*; + /// The reported path is read by a machine that has to find the file again: + /// GitHub matches an annotation by `file=`, and a SARIF reader resolves + /// `artifactLocation.uri` against the checkout. A Windows separator and a + /// `.` segment both name a file no checkout has. + #[test] + fn report_uri_spells_a_path_the_way_a_repository_does() { + assert_eq!(report_uri(Path::new("./a.rs")), "a.rs"); + assert_eq!(report_uri(Path::new("sub/./doc.rs")), "sub/doc.rs"); + assert_eq!(report_uri(Path::new("./sub/./doc.rs")), "sub/doc.rs"); + assert_eq!(report_uri(Path::new(r"sub\doc.rs")), "sub/doc.rs"); + assert_eq!(report_uri(Path::new(r".\sub\.\doc.rs")), "sub/doc.rs"); + // A path that leaves the tree, an absolute one, and standard input are + // all left as they are; only the separators are normalised. + assert_eq!(report_uri(Path::new("../sibling/a.rs")), "../sibling/a.rs"); + assert_eq!(report_uri(Path::new("/tmp/a.rs")), "/tmp/a.rs"); + assert_eq!(report_uri(Path::new(r"C:\src\a.rs")), "C:/src/a.rs"); + assert_eq!(report_uri(Path::new(STDIN_PATH)), STDIN_PATH); + // Naming the working directory as nothing at all would be worse. + assert_eq!(report_uri(Path::new(".")), "."); + } + + /// `%SRCROOT%` says the path is measured from the root of the checkout, so + /// it is claimed only for the paths that are. + #[test] + fn only_a_path_inside_the_tree_is_reported_against_the_source_root() { + for inside in ["a.rs", "sub/doc.rs", "./sub/doc.rs"] { + assert_eq!( + artifact_location(Path::new(inside))["uriBaseId"], + json!(SRCROOT), + "`{inside}` is not reported against the source root" + ); + } + for outside in ["../sibling/a.rs", "/tmp/a.rs", STDIN_PATH] { + let location = artifact_location(Path::new(outside)); + assert_eq!( + location.get("uriBaseId"), + None, + "`{outside}` claims to be under the source root" + ); + } + } + + /// Every result points into the rules by index, so the two orders have to + /// be the same one. + #[test] + fn a_rule_is_described_once_and_keeps_its_index() { + let mut rules = SarifRules::new(); + assert_eq!(rules.entries.len(), CommentKind::ALL.len()); + assert_eq!(rules.kind(CommentKind::Line), 0); + let first = rules.describe("io-error", "error", "short", "full", TOOL_INFORMATION_URI); + assert_eq!(first, CommentKind::ALL.len()); + let again = rules.describe("io-error", "note", "other", "other", TOOL_INFORMATION_URI); + assert_eq!(first, again, "a second sighting described the rule twice"); + assert_eq!( + rules.entries[first]["defaultConfiguration"]["level"], + "error" + ); + assert_eq!(rules.entries.len(), CommentKind::ALL.len() + 1); + } + + #[test] + fn a_diagnostic_code_reads_back_as_a_title() { + assert_eq!( + sentence_case("unterminated-comment"), + "Unterminated comment" + ); + assert_eq!(sentence_case("nesting-limit"), "Nesting limit"); + assert_eq!(sentence_case(""), ""); + } + fn preview_of(source: &[u8], max_columns: usize) -> String { preview(source, ByteSpan::new(0, source.len()), max_columns) } @@ -1412,7 +1670,9 @@ mod tests { } /// The interactive verdict counts answers, and every noun agrees with the - /// number in front of it. + /// number in front of it. It closes on the same `(N files scanned)` the + /// plain `fix` summary ends with: the reader still has to be told how much + /// was looked at to reach the answers. #[test] fn the_interactive_summary_pluralizes_both_of_its_nouns() { assert_eq!( @@ -1423,7 +1683,7 @@ mod tests { changed: 1, scanned: 1, }), - "Removed 1 of 1 comment in 1 file." + "Removed 1 of 1 comment in 1 file (1 file scanned)." ); assert_eq!( interactive_summary(InteractiveOutcome { @@ -1433,7 +1693,7 @@ mod tests { changed: 3, scanned: 4, }), - "Removed 2 of 5 comments in 3 files." + "Removed 2 of 5 comments in 3 files (4 files scanned)." ); } @@ -1472,7 +1732,7 @@ mod tests { changed: 1, scanned: 4, }), - "Removed 1 of 2 comments in 1 file (7 comments not reviewed)." + "Removed 1 of 2 comments in 1 file (7 comments not reviewed) (4 files scanned)." ); assert_eq!( interactive_summary(InteractiveOutcome { @@ -1482,7 +1742,7 @@ mod tests { changed: 0, scanned: 1, }), - "Removed 0 of 1 comment in 0 files (1 comment not reviewed)." + "Removed 0 of 1 comment in 0 files (1 comment not reviewed) (1 file scanned)." ); } diff --git a/rust/ocomment/tests/cli.rs b/rust/ocomment/tests/cli.rs index 92d1bf7..46e81b6 100644 --- a/rust/ocomment/tests/cli.rs +++ b/rust/ocomment/tests/cli.rs @@ -892,6 +892,87 @@ fn a_bare_run_never_reaches_into_the_git_directory() { ); } +/// Naming the directory lifts the hidden-file rule, and so does `files.hidden`; +/// neither may lift the one that keeps git's own storage out of a walk. `git` +/// itself never offers `.git` as a candidate for anything, and a tool that +/// rewrites files in place may do so least of all: `ocomment fix .` in a fresh +/// repository would otherwise rewrite every sample hook git had just written. +#[test] +fn a_named_directory_never_reaches_into_the_git_directory() { + for configuration in ["version = 1\n", "version = 1\n[files]\nhidden = true\n"] { + let directory = tempfile::tempdir().unwrap(); + git(directory.path(), &["init", "-q"]); + fs::write(directory.path().join(".ocomment.toml"), configuration).unwrap(); + let hook = directory.path().join(".git/hooks/x.sample"); + fs::write(&hook, b"#!/bin/sh\necho hi # sample hook comment\n").unwrap(); + let before = fs::read(&hook).unwrap(); + // A submodule or a linked worktree keeps its `.git` as a *file*; it + // points at git's storage and is no more a candidate than the + // directory it stands in for. + fs::create_dir(directory.path().join("vendor")).unwrap(); + fs::write( + directory.path().join("vendor/.git"), + b"gitdir: ../.git/modules/vendor\n", + ) + .unwrap(); + fs::write(directory.path().join("a.rs"), b"let a = 1; // remove me\n").unwrap(); + + let checked = run(directory.path(), &["check", "-v", "."]); + let listing = format!( + "{}{}", + String::from_utf8_lossy(&checked.stdout), + String::from_utf8_lossy(&checked.stderr) + ); + assert!( + !listing.contains(".git"), + "`check .` under {configuration:?} reached into git's storage:\n{listing}" + ); + assert!( + listing.contains("a.rs:1:12: removable line comment"), + "`check .` under {configuration:?} missed the project file:\n{listing}" + ); + + let fixed = run(directory.path(), &["fix", "."]); + let report = format!( + "{}{}", + String::from_utf8_lossy(&fixed.stdout), + String::from_utf8_lossy(&fixed.stderr) + ); + assert!( + !report.contains(".git"), + "`fix .` under {configuration:?} reported something under .git:\n{report}" + ); + assert_eq!( + fs::read(&hook).unwrap(), + before, + "`fix .` under {configuration:?} rewrote a file under .git" + ); + } +} + +/// The exclusion is about where a walk may wander, not about what a caller may +/// ask for. A path typed on the command line is a request, so a hook the +/// caller pointed at is still reported. +#[test] +fn a_path_named_inside_the_git_directory_is_still_honoured() { + let directory = tempfile::tempdir().unwrap(); + git(directory.path(), &["init", "-q"]); + let hook = directory.path().join(".git/hooks/x.sample"); + fs::write(&hook, b"#!/bin/sh\necho hi # sample hook comment\n").unwrap(); + + let output = run(directory.path(), &["check", ".git/hooks/x.sample"]); + let listing = String::from_utf8_lossy(&output.stdout).into_owned(); + assert_eq!( + output.status.code(), + Some(1), + "a named hook was not reported:\n{listing}" + ); + assert!( + listing.contains(".git/hooks/x.sample:2:9: removable line comment"), + "a named hook was not reported:\n{listing}" + ); +} + #[cfg(unix)] #[test] fn symlink_following_is_explicitly_configurable() { @@ -1307,6 +1388,294 @@ fn sarif_keeps_kebab_rule_ids_and_canonical_messages() { assert_no_debug_leak("SARIF report", &String::from_utf8(output.stdout).unwrap()); } +/// Every comment kind a rule id can name, in the spelling `CommentKind` +/// serialises. A kind added without a rule to describe it fails this test. +const SARIF_KINDS: [&str; 11] = [ + "line", + "block", + "doc-line", + "doc-block", + "directive", + "license", + "html-comment", + "shebang", + "encoding", + "optimizer-hint", + "version-comment", +]; + +/// The SARIF failure levels OComment reports at. `none` is a level too, but +/// nothing OComment writes uses it. +const SARIF_LEVELS: [&str; 3] = ["error", "warning", "note"]; + +/// A code-scanning UI shows a finding through the rule it names: the title, the +/// sentence under it, and the link it offers all come from +/// `tool.driver.rules`, which a result reaches by `ruleIndex`. A rule the tool +/// never describes leaves the finding with nothing but its id, so every id a +/// run can emit is described and every result points at its own description. +#[test] +fn sarif_describes_every_rule_it_reports() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("a.rs"), b"let value = 1; // remove\n").unwrap(); + fs::write(directory.path().join("bad.rs"), b"/* never ends\n").unwrap(); + fs::write(directory.path().join("plain.txt"), b"nothing to scan\n").unwrap(); + let output = run( + directory.path(), + &["check", "a.rs", "bad.rs", "plain.txt", "--format", "sarif"], + ); + assert_eq!(output.status.code(), Some(2)); + let report = String::from_utf8(output.stdout).unwrap(); + let document: serde_json::Value = serde_json::from_str(&report).unwrap(); + let driver = &document["runs"][0]["tool"]["driver"]; + assert_eq!(driver["name"], "ocomment"); + assert_eq!( + driver["version"], + env!("CARGO_PKG_VERSION"), + "the driver does not report the version that produced the run:\n{report}" + ); + assert_eq!( + driver["informationUri"], + "https://github.com/P4suta/OComment" + ); + + let rules = driver["rules"] + .as_array() + .unwrap_or_else(|| panic!("tool.driver.rules is not an array:\n{report}")); + let mut described = BTreeSet::new(); + for rule in rules { + let id = rule["id"] + .as_str() + .unwrap_or_else(|| panic!("a rule has no string id:\n{report}")); + assert!(described.insert(id.to_owned()), "`{id}` is described twice"); + for field in ["shortDescription", "fullDescription"] { + let text = rule[field]["text"].as_str().unwrap_or_default(); + assert!(!text.is_empty(), "rule `{id}` has no {field}:\n{report}"); + } + let help = rule["helpUri"].as_str().unwrap_or_default(); + assert!( + help.starts_with("https://"), + "rule `{id}` links nowhere: {help:?}" + ); + let level = rule["defaultConfiguration"]["level"] + .as_str() + .unwrap_or_default(); + assert!( + SARIF_LEVELS.contains(&level), + "rule `{id}` defaults to {level:?}, which is not a SARIF level" + ); + } + let removable: BTreeSet = described + .iter() + .filter(|id| id.starts_with("removable-")) + .cloned() + .collect(); + let expected: BTreeSet = SARIF_KINDS + .iter() + .map(|kind| format!("removable-{kind}")) + .collect(); + assert_eq!( + removable, expected, + "the rules do not describe exactly one removable kind each" + ); + let doc_block = rules + .iter() + .find(|rule| rule["id"] == "removable-doc-block") + .expect("`removable-doc-block` is described"); + assert_eq!( + doc_block["shortDescription"]["text"], + "Removable doc-block comment" + ); + assert_eq!(doc_block["defaultConfiguration"]["level"], "note"); + + let results = document["runs"][0]["results"].as_array().unwrap(); + let mut reported = BTreeSet::new(); + for result in results { + let id = result["ruleId"] + .as_str() + .unwrap_or_else(|| panic!("a result has no ruleId:\n{report}")); + let index = result["ruleIndex"] + .as_u64() + .unwrap_or_else(|| panic!("the `{id}` result has no ruleIndex:\n{report}")); + assert_eq!( + rules[index as usize]["id"], id, + "the `{id}` result points at rule {index}, which describes something else" + ); + let level = result["level"].as_str().unwrap_or_default(); + assert!( + SARIF_LEVELS.contains(&level), + "the `{id}` result is at level {level:?}, which is not a SARIF level" + ); + reported.insert(id.to_owned()); + } + for id in [ + "removable-line", + "removable-block", + "unterminated-comment", + "skipped-file", + ] { + assert!( + reported.contains(id), + "the run reported no `{id}` result:\n{report}" + ); + } + assert_no_debug_leak("SARIF report", &report); +} + +/// A file OComment cannot read is reported as a result too, and it names a rule +/// like any other finding. +#[cfg(unix)] +#[test] +fn sarif_describes_the_io_error_rule_when_it_reports_one() { + use std::os::unix::fs::PermissionsExt; + + let directory = tempfile::tempdir().unwrap(); + let unreadable = directory.path().join("locked.rs"); + fs::write(&unreadable, b"let value = 1; // remove\n").unwrap(); + fs::set_permissions(&unreadable, fs::Permissions::from_mode(0o000)).unwrap(); + let output = run( + directory.path(), + &["check", "locked.rs", "--format", "sarif"], + ); + assert_eq!(output.status.code(), Some(2)); + let report = String::from_utf8(output.stdout).unwrap(); + let document: serde_json::Value = serde_json::from_str(&report).unwrap(); + let result = &document["runs"][0]["results"][0]; + assert_eq!(result["ruleId"], "io-error"); + assert_eq!(result["level"], "error"); + let index = result["ruleIndex"] + .as_u64() + .unwrap_or_else(|| panic!("the io-error result has no ruleIndex:\n{report}")); + let rule = &document["runs"][0]["tool"]["driver"]["rules"][index as usize]; + assert_eq!(rule["id"], "io-error"); + assert_eq!(rule["defaultConfiguration"]["level"], "error"); +} + +/// A code-scanning UI resolves `artifactLocation.uri` against the checkout, so +/// a reported path has to be spelled the way the repository spells it: forward +/// slashes, no `./` standing in for the directory the run started in, and +/// `%SRCROOT%` saying what the rest is relative to. Every location in the +/// document is read that way, the ones under `fixes` included. +#[test] +fn sarif_locates_reported_files_under_the_source_root() { + let directory = tempfile::tempdir().unwrap(); + fs::create_dir(directory.path().join("sub")).unwrap(); + fs::write( + directory.path().join("sub/doc.rs"), + b"/** doc */\nfn main() {}\n", + ) + .unwrap(); + let output = run( + directory.path(), + &["check", "sub/./doc.rs", "--format", "sarif"], + ); + assert_eq!(output.status.code(), Some(1)); + let report = String::from_utf8(output.stdout).unwrap(); + let document: serde_json::Value = serde_json::from_str(&report).unwrap(); + let locations = artifact_locations(&document); + assert!( + !locations.is_empty(), + "the report locates nothing:\n{report}" + ); + for location in &locations { + let uri = location["uri"].as_str().unwrap_or_default(); + assert_eq!(uri, "sub/doc.rs", "a location is spelled {uri:?}"); + assert_eq!( + location["uriBaseId"], "%SRCROOT%", + "a relative location says nothing about what it is relative to:\n{report}" + ); + } +} + +/// A path the user typed as an absolute one is not under the checkout, so it +/// keeps its absolute spelling and names no base id — a base id would say it is +/// relative to the source root, which it is not. +#[test] +fn sarif_leaves_an_absolute_path_absolute_and_unbased() { + let directory = tempfile::tempdir().unwrap(); + let absolute = directory.path().join("a.rs"); + fs::write(&absolute, b"let value = 1; // remove\n").unwrap(); + let output = run( + directory.path(), + &["check", absolute.to_str().unwrap(), "--format", "sarif"], + ); + assert_eq!(output.status.code(), Some(1)); + let report = String::from_utf8(output.stdout).unwrap(); + let document: serde_json::Value = serde_json::from_str(&report).unwrap(); + let expected = absolute.to_string_lossy().replace('\\', "/"); + for location in artifact_locations(&document) { + assert_eq!(location["uri"], expected); + assert!( + location.get("uriBaseId").is_none(), + "an absolute location claims a base id:\n{report}" + ); + } +} + +/// Standard input has no place in the checkout either, so the pseudo-path it is +/// reported under is left alone rather than resolved against the source root. +#[test] +fn sarif_leaves_the_stdin_pseudo_path_unbased() { + let directory = tempfile::tempdir().unwrap(); + let output = run_stdin( + directory.path(), + &["check", "-", "--language", "rust", "--format", "sarif"], + b"let value = 1; // remove\n", + ); + assert_eq!(output.status.code(), Some(1)); + let report = String::from_utf8(output.stdout).unwrap(); + let document: serde_json::Value = serde_json::from_str(&report).unwrap(); + for location in artifact_locations(&document) { + assert_eq!(location["uri"], ""); + assert!( + location.get("uriBaseId").is_none(), + "the standard-input pseudo-path claims a base id:\n{report}" + ); + } +} + +/// Every `artifactLocation` in a SARIF document, from the locations a result +/// reports and from the changes its fix would make. +fn artifact_locations(document: &serde_json::Value) -> Vec { + let mut found = Vec::new(); + for run in document["runs"].as_array().into_iter().flatten() { + for result in run["results"].as_array().into_iter().flatten() { + for location in result["locations"].as_array().into_iter().flatten() { + found.push(location["physicalLocation"]["artifactLocation"].clone()); + } + for fix in result["fixes"].as_array().into_iter().flatten() { + for change in fix["artifactChanges"].as_array().into_iter().flatten() { + found.push(change["artifactLocation"].clone()); + } + } + } + } + found +} + +/// GitHub matches an annotation to a line of the diff by the path in `file=`, +/// and matches it against the paths the repository uses. A `./` the walk left +/// behind is enough to lose the annotation. +#[test] +fn github_annotations_report_repository_paths() { + let directory = tempfile::tempdir().unwrap(); + fs::create_dir(directory.path().join("sub")).unwrap(); + fs::write( + directory.path().join("sub/doc.rs"), + b"/** doc */\nfn main() {}\n", + ) + .unwrap(); + let output = run( + directory.path(), + &["check", "sub/./doc.rs", "--format", "github"], + ); + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert_eq!( + stdout, + "::notice file=sub/doc.rs,line=1,col=1::removable doc-block comment\n" + ); +} + #[test] fn json_and_jsonl_serde_names_are_frozen() { let directory = tempfile::tempdir().unwrap(); @@ -1861,6 +2230,81 @@ fn a_previewed_comment_cannot_inject_escape_sequences() { ); } +/// A file name is chosen by whoever made the file, so the half of a report +/// line that shows a path is untrusted input on its way to a terminal exactly +/// like the preview beside it. It gets the same treatment, and is cut nowhere: +/// a path ending in an ellipsis names no file. +#[cfg(unix)] +#[test] +fn a_reported_path_cannot_inject_escape_sequences() { + use std::os::unix::ffi::OsStringExt; + + let directory = tempfile::tempdir().unwrap(); + let name = std::ffi::OsString::from_vec(b"evil\x1b[2Jname.rs".to_vec()); + fs::write(directory.path().join(&name), b"let x = 1; // remove me\n").unwrap(); + let unreadable = std::ffi::OsString::from_vec(b"evil\x1b[2Jskip.bin".to_vec()); + fs::write(directory.path().join(&unreadable), b"\x00\x01binary\n").unwrap(); + + let output = run(directory.path(), &["check", "-v", "."]); + assert_eq!( + output.status.code(), + Some(1), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + !output.stdout.contains(&0x1b), + "an escape byte reached the report: {:?}", + String::from_utf8_lossy(&output.stdout) + ); + assert!( + !output.stderr.contains(&0x1b), + "an escape byte reached the summary: {:?}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains("evil\u{fffd}[2Jname.rs:1:12: removable line comment"), + "the report lost the file it names:\n{stdout}" + ); + assert!( + stdout.contains("evil\u{fffd}[2Jskip.bin: skipped: binary file"), + "the skip lost the file it names:\n{stdout}" + ); +} + +/// A configuration file is read from the project, and the pattern in it is +/// echoed back on the line that rejects it. That makes it untrusted input on +/// its way to a terminal, and it is folded like every other one. +#[test] +fn an_invalid_policy_regex_cannot_inject_escape_sequences() { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join("a.rs"), b"let x = 1; // remove me\n").unwrap(); + fs::write( + directory.path().join(".ocomment.toml"), + "version = 1\n[policy]\nkeep_regex = [\"\\u001B[2J(\"]\n", + ) + .unwrap(); + + let output = run(directory.path(), &["check", "a.rs"]); + assert_eq!( + output.status.code(), + Some(2), + "an invalid regex was accepted:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + !output.stderr.contains(&0x1b), + "an escape byte reached the terminal: {:?}", + String::from_utf8_lossy(&output.stderr) + ); + let error = String::from_utf8(output.stderr).unwrap(); + assert!( + error.contains("invalid comment policy regex `\u{fffd}[2J(`"), + "the error lost the pattern it rejects:\n{error}" + ); +} + /// A long comment is cut to a readable width rather than flooding the report. #[test] fn a_long_comment_preview_is_truncated_with_an_ellipsis() { @@ -3624,18 +4068,27 @@ fn explain_is_refused_by_every_machine_format() { } /// `--explain` annotates a report of comments, and only `check` and `scan` -/// write one: `fix` reports the files it rewrote, `diff` writes a patch, and -/// `strip` writes the stripped source. The flag is global, so asking for it -/// there is a usage error rather than a flag that quietly does nothing. +/// write one: `fix` reports the files it rewrote, `diff` writes a patch, +/// `strip` writes the stripped source, and the rest of the commands are not +/// about comments at all. The flag is global, so asking for it anywhere else +/// is a usage error rather than a flag that quietly does nothing. #[test] fn explain_is_refused_by_the_commands_that_write_no_report() { let directory = tempfile::tempdir().unwrap(); fs::write(directory.path().join("a.rs"), b"let x = 1; // TODO\n").unwrap(); - let refused: [&[&str]; 4] = [ + let refused: [&[&str]; 12] = [ &["fix", "--explain", "--dry-run"], &["fix", "--explain"], &["diff", "--explain"], &["strip", "--explain", "--language", "rust"], + &["lsp", "--explain"], + &["init", "--explain"], + &["config", "--explain"], + &["languages", "--explain"], + &["plugin", "--explain", "list"], + &["completions", "--explain", "bash"], + &["doctor", "--explain"], + &["man", "--explain"], ]; for arguments in refused { let output = run_stdin(directory.path(), arguments, b"let x = 1; // TODO\n"); @@ -3662,6 +4115,10 @@ fn explain_is_refused_by_the_commands_that_write_no_report() { b"let x = 1; // TODO\n", "a refused run rewrote the file anyway" ); + assert!( + !directory.path().join(".ocomment.toml").exists(), + "a refused `init` wrote its starter file anyway" + ); // The two commands the flag is for still take it. for command in ["check", "scan"] { let output = run(directory.path(), &[command, "--explain"]); diff --git a/tools/check_hooks.py b/tools/check_hooks.py new file mode 100644 index 0000000..533c4c7 --- /dev/null +++ b/tools/check_hooks.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +"""Fail when .pre-commit-hooks.yaml drifts from the canonical language table. + +The `files:` pattern of every published pre-commit hook must select exactly the +extensions in `spec/languages.toml`, so adding a language to the shared spec +cannot silently leave the hooks scanning the old file set. Only the standard +library is used, because this runs next to `tools/check_embedded_specs.py` in a +job that installs nothing. +""" + +from __future__ import annotations + +import argparse +import pathlib +import re +import tomllib + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +LANGUAGES = ROOT / "spec/languages.toml" +HOOKS = ROOT / ".pre-commit-hooks.yaml" + +EXPECTED_ENTRIES = { + "ocomment-check": "ocomment check", + "ocomment-fix": "ocomment fix", +} + +# pre-commit rejects an unknown manifest key only when a consumer runs the hook, +# so a typo here would ship broken. These are the keys its manifest schema takes. +KNOWN_FIELDS = frozenset( + { + "additional_dependencies", + "alias", + "always_run", + "args", + "description", + "entry", + "exclude", + "exclude_types", + "fail_fast", + "files", + "id", + "language", + "language_version", + "log_file", + "minimum_pre_commit_version", + "name", + "pass_filenames", + "require_serial", + "stages", + "types", + "types_or", + "verbose", + } +) +REQUIRED_FIELDS = ("id", "name", "entry", "language") + + +def expected_files_pattern() -> str: + """Build the `files:` regex that matches every extension in the spec.""" + with LANGUAGES.open("rb") as stream: + table = tomllib.load(stream) + extensions: set[str] = set() + for language in table["languages"]: + for extension in language["extensions"]: + if not re.fullmatch(r"[a-z0-9]+", extension): + raise SystemExit( + f"extension {extension!r} needs regex quoting; update {__file__}" + ) + extensions.add(extension) + if not extensions: + raise SystemExit(f"{LANGUAGES} lists no extensions") + return r"(?i)\.(" + "|".join(sorted(extensions)) + r")$" + + +def unquote(value: str) -> str: + """Undo the single- or double-quoted YAML scalar forms this file uses.""" + if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"": + inner = value[1:-1] + return inner.replace("''", "'") if value[0] == "'" else inner + return value + + +def parse_hooks(text: str) -> list[dict[str, str]]: + """Parse the flat `- key: value` hook list without a YAML dependency.""" + hooks: list[dict[str, str]] = [] + for number, line in enumerate(text.splitlines(), start=1): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + if line.startswith("- "): + hooks.append({}) + stripped = stripped[2:].strip() + if not stripped: + continue + elif not line.startswith(" "): + raise SystemExit(f"{HOOKS.name}:{number}: unexpected top-level line") + if not hooks: + raise SystemExit(f"{HOOKS.name}:{number}: value outside a hook entry") + key, separator, value = stripped.partition(":") + if not separator: + raise SystemExit(f"{HOOKS.name}:{number}: expected `key: value`") + hooks[-1][key.strip()] = unquote(value.strip()) + return hooks + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "--print-pattern", + action="store_true", + help="write the expected `files:` regex to stdout and exit", + ) + args = parser.parse_args() + + pattern = expected_files_pattern() + if args.print_pattern: + print(pattern) + return 0 + + if not HOOKS.is_file(): + print(f"{HOOKS.relative_to(ROOT)} is missing") + return 1 + + failures: list[str] = [] + parsed = parse_hooks(HOOKS.read_text()) + for position, hook in enumerate(parsed): + label = hook.get("id") or f"#{position}" + for field in sorted(set(hook) - KNOWN_FIELDS): + failures.append(f"hook `{label}` has `{field}`, which pre-commit does not define") + for field in REQUIRED_FIELDS: + if not hook.get(field): + failures.append(f"hook `{label}` is missing the required `{field}`") + + hooks = {hook.get("id", ""): hook for hook in parsed} + for hook_id, entry in EXPECTED_ENTRIES.items(): + hook = hooks.get(hook_id) + if hook is None: + failures.append(f"hook `{hook_id}` is missing") + continue + if hook.get("entry") != entry: + failures.append( + f"hook `{hook_id}` entry is {hook.get('entry')!r}, expected {entry!r}" + ) + if hook.get("language") != "system": + failures.append( + f"hook `{hook_id}` language is {hook.get('language')!r}, expected 'system'" + " (pre-commit's `language: rust` builds the checkout root, and the" + " manifest lives in rust/)" + ) + actual = hook.get("files") + if actual != pattern: + failures.append( + f"hook `{hook_id}` files is {actual!r}\n" + f"{' ' * 4}spec/languages.toml requires {pattern!r}" + ) + + if failures: + print("\n".join(failures)) + print("Regenerate with: python3 tools/check_hooks.py --print-pattern") + return 1 + print(f"{len(EXPECTED_ENTRIES)} pre-commit hooks match spec/languages.toml") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/release_manifests.py b/tools/release_manifests.py index 5d2d680..413ad24 100644 --- a/tools/release_manifests.py +++ b/tools/release_manifests.py @@ -143,7 +143,10 @@ def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--release-dir", required=True, type=pathlib.Path) parser.add_argument("--version", required=True) - parser.add_argument("--repository", default="ocomment/ocomment") + # The release workflow passes $GITHUB_REPOSITORY; the default is for a + # person generating the definitions by hand, and it has to name the + # repository the archives are actually published from. + parser.add_argument("--repository", default="P4suta/OComment") args = parser.parse_args() version = args.version.removeprefix("v") diff --git a/tools/validate_schemas.py b/tools/validate_schemas.py index e21c937..8d2756e 100755 --- a/tools/validate_schemas.py +++ b/tools/validate_schemas.py @@ -10,11 +10,223 @@ import tempfile import tomllib -import jsonschema - ROOT = pathlib.Path(__file__).resolve().parents[1] +SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json" +SARIF_LEVELS = frozenset({"none", "note", "warning", "error"}) +# What a repository-relative artifact URI is measured from, and the pseudo-path +# a run over standard input reports, which is measured from nothing. +SARIF_SRCROOT = "%SRCROOT%" +SARIF_STDIN_URI = "" + + +def check_sarif_region(region: object, where: str, failures: list[str]) -> None: + """SARIF text regions are 1-based and must not run backwards.""" + if not isinstance(region, dict): + failures.append(f"{where} is not an object") + return + for field in ("startLine", "startColumn", "endLine", "endColumn"): + value = region.get(field) + if not isinstance(value, int) or isinstance(value, bool) or value < 1: + failures.append(f"{where}.{field} is not a 1-based integer: {value!r}") + start_line, end_line = region.get("startLine"), region.get("endLine") + if isinstance(start_line, int) and isinstance(end_line, int) and end_line < start_line: + failures.append(f"{where} ends on line {end_line} before line {start_line}") + + +def check_sarif_uri(location: object, where: str, failures: list[str]) -> None: + if not isinstance(location, dict): + failures.append(f"{where} is not an object") + return + uri = location.get("uri") + base = location.get("uriBaseId") + if not isinstance(uri, str) or not uri: + failures.append(f"{where}.uri is not a non-empty string: {uri!r}") + return + if uri.startswith("/") or "://" in uri: + failures.append(f"{where}.uri is not repository-relative: {uri!r}") + return + # A code-scanning UI matches the URI against the paths the checkout uses, + # and neither a backslash nor a `.` segment names a file any checkout has. + if "\\" in uri: + failures.append(f"{where}.uri uses a backslash separator: {uri!r}") + if uri.startswith("./") or "/./" in uri: + failures.append(f"{where}.uri keeps a `.` segment: {uri!r}") + # A relative URI resolves against a base id; standard input and a path that + # climbs out of the tree are under no base at all. + if uri == SARIF_STDIN_URI or uri.startswith("../"): + if base is not None: + failures.append(f"{where}.uriBaseId is {base!r}, but {uri!r} is under no base") + elif base != SARIF_SRCROOT: + failures.append(f"{where}.uriBaseId is {base!r}, expected {SARIF_SRCROOT!r}") + + +def check_sarif_rules(driver: dict, where: str, failures: list[str]) -> dict[str, int]: + """The rule catalogue, as the id-to-index map a result is checked against. + + A code-scanning UI titles a finding, describes it, and links out of it + through the rule the result names, so a result whose rule is missing -- + or whose `ruleIndex` points at somebody else's -- arrives with nothing but + its id. + """ + rules = driver.get("rules") + if not isinstance(rules, list) or not rules: + failures.append(f"{where}.rules is not a non-empty array") + return {} + indices: dict[str, int] = {} + for index, rule in enumerate(rules): + spot = f"{where}.rules[{index}]" + if not isinstance(rule, dict): + failures.append(f"{spot} is not an object") + continue + identifier = rule.get("id") + if not isinstance(identifier, str) or not identifier: + failures.append(f"{spot}.id is not a non-empty string: {identifier!r}") + continue + if identifier in indices: + failures.append(f"{spot}.id describes `{identifier}` a second time") + else: + indices[identifier] = index + for field in ("shortDescription", "fullDescription"): + described = rule.get(field) + text = described.get("text") if isinstance(described, dict) else None + if not isinstance(text, str) or not text: + failures.append(f"{spot}.{field}.text is not a non-empty string: {text!r}") + help_uri = rule.get("helpUri") + if not isinstance(help_uri, str) or not help_uri.startswith("https://"): + failures.append(f"{spot}.helpUri is not an https URL: {help_uri!r}") + configuration = rule.get("defaultConfiguration") + level = configuration.get("level") if isinstance(configuration, dict) else None + if level not in SARIF_LEVELS: + failures.append(f"{spot}.defaultConfiguration.level is {level!r}, not a SARIF level") + return indices + + +def check_sarif(path: pathlib.Path) -> int: + """Structurally validate an `ocomment --format sarif` document. + + The published SARIF schema is only reachable over the network -- and the + job that runs this on three operating systems installs no Python packages + -- so this checks the shape and the invariants OComment guarantees + instead: the driver names itself and its version, every rule a result + names is described at the index the result points at, and every reported + path is spelled the way the checkout spells it. + """ + failures: list[str] = [] + try: + document = json.loads(path.read_text()) + except (OSError, json.JSONDecodeError) as error: + print(f"{path}: {error}") + return 1 + + if not isinstance(document, dict): + print(f"{path}: top level is not an object") + return 1 + if document.get("version") != "2.1.0": + failures.append(f"version is {document.get('version')!r}, expected '2.1.0'") + if document.get("$schema") != SARIF_SCHEMA: + failures.append(f"$schema is {document.get('$schema')!r}, expected {SARIF_SCHEMA!r}") + + runs = document.get("runs") + if not isinstance(runs, list) or not runs: + print(f"{path}: runs is not a non-empty array") + return 1 + + results_seen = 0 + rules_seen = 0 + for index, run in enumerate(runs): + where = f"runs[{index}]" + if not isinstance(run, dict): + failures.append(f"{where} is not an object") + continue + driver = run.get("tool", {}).get("driver", {}) if isinstance(run.get("tool"), dict) else {} + rules: dict[str, int] = {} + if not isinstance(driver, dict) or driver.get("name") != "ocomment": + failures.append(f"{where}.tool.driver.name is not 'ocomment'") + else: + if not isinstance(driver.get("informationUri"), str): + failures.append(f"{where}.tool.driver.informationUri is missing") + version = driver.get("version") + if not isinstance(version, str) or not version: + failures.append(f"{where}.tool.driver.version is not a non-empty string: {version!r}") + rules = check_sarif_rules(driver, f"{where}.tool.driver", failures) + rules_seen += len(rules) + results = run.get("results") + if not isinstance(results, list): + failures.append(f"{where}.results is not an array") + continue + for position, result in enumerate(results): + results_seen += 1 + spot = f"{where}.results[{position}]" + if not isinstance(result, dict): + failures.append(f"{spot} is not an object") + continue + rule_id = result.get("ruleId") + if not isinstance(rule_id, str) or not rule_id: + failures.append(f"{spot}.ruleId is not a non-empty string") + else: + rule_index = result.get("ruleIndex") + if not isinstance(rule_index, int) or isinstance(rule_index, bool): + failures.append(f"{spot}.ruleIndex is not an integer: {rule_index!r}") + elif rule_id not in rules: + failures.append(f"{spot}.ruleId `{rule_id}` is described by no rule") + elif rules[rule_id] != rule_index: + failures.append( + f"{spot}.ruleIndex is {rule_index}, " + f"but `{rule_id}` is rules[{rules[rule_id]}]" + ) + message = result.get("message") + if not isinstance(message, dict) or not isinstance(message.get("text"), str) or not message["text"]: + failures.append(f"{spot}.message.text is not a non-empty string") + if result.get("level") not in SARIF_LEVELS: + failures.append(f"{spot}.level is {result.get('level')!r}, not a SARIF level") + locations = result.get("locations") + if not isinstance(locations, list) or not locations: + failures.append(f"{spot}.locations is not a non-empty array") + continue + for slot, location in enumerate(locations): + place = f"{spot}.locations[{slot}].physicalLocation" + physical = location.get("physicalLocation") if isinstance(location, dict) else None + if not isinstance(physical, dict): + failures.append(f"{place} is not an object") + continue + check_sarif_uri(physical.get("artifactLocation"), f"{place}.artifactLocation", failures) + if "region" in physical: + check_sarif_region(physical["region"], f"{place}.region", failures) + for slot, fix in enumerate(result.get("fixes", []) or []): + place = f"{spot}.fixes[{slot}]" + changes = fix.get("artifactChanges") if isinstance(fix, dict) else None + if not isinstance(changes, list) or not changes: + failures.append(f"{place}.artifactChanges is not a non-empty array") + continue + for offset, change in enumerate(changes): + corner = f"{place}.artifactChanges[{offset}]" + if not isinstance(change, dict): + failures.append(f"{corner} is not an object") + continue + check_sarif_uri(change.get("artifactLocation"), f"{corner}.artifactLocation", failures) + replacements = change.get("replacements") + if not isinstance(replacements, list) or not replacements: + failures.append(f"{corner}.replacements is not a non-empty array") + continue + for edge, replacement in enumerate(replacements): + if not isinstance(replacement, dict): + failures.append(f"{corner}.replacements[{edge}] is not an object") + continue + check_sarif_region( + replacement.get("deletedRegion"), + f"{corner}.replacements[{edge}].deletedRegion", + failures, + ) + + if failures: + print(f"{path}:") + print("\n".join(f" {failure}" for failure in failures)) + return 1 + print(f"{path}: valid SARIF 2.1.0 with {results_seen} ocomment results under {rules_seen} rules") + return 0 + def main() -> int: parser = argparse.ArgumentParser() @@ -23,7 +235,17 @@ def main() -> int: type=pathlib.Path, default=ROOT / "rust/target/debug/ocomment", ) + parser.add_argument( + "--sarif", + type=pathlib.Path, + help="validate this `--format sarif` document instead of the canonical schemas", + ) args = parser.parse_args() + if args.sarif is not None: + return check_sarif(args.sarif) + + import jsonschema + config_schema = json.loads((ROOT / "spec/config.schema.json").read_text()) result_schema = json.loads((ROOT / "spec/result.schema.json").read_text()) jsonschema.Draft202012Validator.check_schema(config_schema) From 4e49ee5e557d17429045e7416509dfff6b0f90d0 Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Wed, 26 Aug 2026 08:49:16 +0900 Subject: [PATCH 07/21] feat(repo): dogfood OComment as a CI gate, container image on GHCR, hyperlink/path hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .ocomment.toml (legal + doc kinds kept + tagged rationale comments via keep_regex), lefthook.yml (check --staged, fmt), CONTRIBUTING tag convention (NOTE/INVARIANT/PERF/TODO/FIXME/HACK; SAFETY reserved for unsafe); explanatory comments across rust/, ocaml/, tools/ and the Dockerfile retagged without changing meaning - ci.yml dogfood job: doctor, config explain, bare `ocomment` must exit 0, and a self-strip copy (fix --policy all --force-protected) must still build and test in Rust and OCaml - --staged honours [files] include/exclude with the same root-relative matching as the walk (the repository's own hook caught this) - scanner: Rust multi-line string literals no longer confuse the scanner (Rust + OCaml reference, pinned by a differential fixture); `# syntax=` and `# hadolint` are protected directives - Dockerfile (rust:1.88-alpine builder → scratch, non-root), release.yml publish-container (multi-arch, provenance, SBOM, cosign, attestation), ci.yml docker smoke incl. the prebuilt-binary context, docs/docker.md - hyperlink targets percent-encode every non-unreserved byte; displayed paths keep their spacing and neutralise control characters; regex errors are folded onto one sanitised line; skipped paths are deduplicated; GitHub annotations fold walked skips like the human renderer; SARIF validator agrees with the emitter and self-tests; action.yml Windows path/unzip fallbacks Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RLSWPMQ9PvtGi3jyBF7bJx --- .dockerignore | 11 + .github/dependabot.yml | 22 ++ .github/rulesets/main.json | 2 + .github/workflows/ci.yml | 112 ++++++ .github/workflows/release.yml | 86 +++++ .ocomment.toml | 20 + CHANGELOG.md | 44 +++ CONTRIBUTING.md | 61 +++ Dockerfile | 37 ++ README.md | 10 +- action.yml | 25 +- docs/docker.md | 137 +++++++ docs/releasing.md | 22 ++ lefthook.yml | 26 ++ ocaml/lib/ocomment_ref.ml | 19 +- ocaml/test/test_core.ml | 7 + rust/ocomment-core/examples/throughput.rs | 6 +- rust/ocomment-core/src/incremental.rs | 42 +-- rust/ocomment-core/src/scanner.rs | 36 +- rust/ocomment-core/src/transform.rs | 4 +- rust/ocomment-core/tests/languages.rs | 49 +++ rust/ocomment/src/atomic.rs | 10 +- rust/ocomment/src/cli.rs | 182 ++++----- rust/ocomment/src/config.rs | 47 +-- rust/ocomment/src/files.rs | 56 ++- rust/ocomment/src/git.rs | 39 +- rust/ocomment/src/interactive.rs | 26 +- rust/ocomment/src/lsp.rs | 2 +- rust/ocomment/src/main.rs | 6 +- rust/ocomment/src/output.rs | 210 ++++++++--- rust/ocomment/src/plugin.rs | 4 +- rust/ocomment/src/values.rs | 8 +- rust/ocomment/tests/cli.rs | 428 +++++++++++++++++++--- rust/ocomment/tests/lsp.rs | 2 +- rust/ocomment/tests/source_guards.rs | 2 +- spec/directives.toml | 3 +- spec/fixtures/README.md | 6 + tools/check_hooks.py | 5 +- tools/differential.py | 8 + tools/release_manifests.py | 6 +- tools/validate_schemas.py | 118 +++++- 41 files changed, 1600 insertions(+), 346 deletions(-) create mode 100644 .dockerignore create mode 100644 .ocomment.toml create mode 100644 Dockerfile create mode 100644 docs/docker.md create mode 100644 lefthook.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e5392e6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +# The image reads only the Rust workspace and the two licences, so deny +# everything and re-admit exactly those. A narrow context also keeps +# `rust/target` — gigabytes on a developer machine — out of the build. +* + +!rust +rust/target +rust/vendor/*/target + +!LICENSE-APACHE +!LICENSE-MIT diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9a05fa8..8e5e2a8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -51,3 +51,25 @@ updates: update-types: - minor - patch + - package-ecosystem: docker + directory: / + schedule: + interval: weekly + day: monday + time: "10:00" + timezone: Asia/Tokyo + cooldown: + default-days: 7 + open-pull-requests-limit: 10 + labels: + - dependencies + - "area: ci" + commit-message: + prefix: "chore(deps)" + ignore: + # The builder stage is pinned to the MSRV toolchain on purpose; a major + # or minor Rust bump is a deliberate change, not a dependency update. + - dependency-name: rust + update-types: + - version-update:semver-major + - version-update:semver-minor diff --git a/.github/rulesets/main.json b/.github/rulesets/main.json index 37ca1cc..632c1ce 100644 --- a/.github/rulesets/main.json +++ b/.github/rulesets/main.json @@ -33,6 +33,8 @@ {"context": "rust"}, {"context": "msrv"}, {"context": "reference"}, + {"context": "docker"}, + {"context": "dogfood"}, {"context": "host-smoke (ubuntu-latest)"}, {"context": "host-smoke (macos-15)"}, {"context": "host-smoke (windows-2025)"}, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af8fd72..82525f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,65 @@ jobs: - run: opam install ./ocaml/ocomment-ref.opam --deps-only --with-test - run: opam exec -- dune runtest --root ocaml - run: opam exec -- ./tools/differential.sh + - run: cargo build --manifest-path rust/Cargo.toml --locked -p ocomment + - name: The reference still builds with every comment stripped out of it + run: | + set -euo pipefail + rm -rf "${RUNNER_TEMP}/strip-ocaml" + mkdir -p "${RUNNER_TEMP}/strip-ocaml" + git ls-files -z ocaml | xargs -0 cp --parents -t "${RUNNER_TEMP}/strip-ocaml" + # Only the strip moves into the copy, and it does so in a subshell: + # it has to start there so it inherits none of this repository's + # `.ocomment.toml`, while `opam exec` resolves the local switch + # setup-ocaml made in the workspace and stops finding it from + # anywhere else. So dune is pointed at the copy instead of moved to + # it, and the step never leaves GITHUB_WORKSPACE. + ( + cd "${RUNNER_TEMP}/strip-ocaml" + "${GITHUB_WORKSPACE}/rust/target/debug/ocomment" \ + fix --policy all --force-protected 2>&1 | tee "${RUNNER_TEMP}/strip-ocaml.log" + ) + grep -qE 'Removed [0-9]+ comments? in [0-9]+ files?' "${RUNNER_TEMP}/strip-ocaml.log" + opam exec -- dune build --root "${RUNNER_TEMP}/strip-ocaml/ocaml" + + dogfood: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable + - run: cargo build --manifest-path rust/Cargo.toml --locked -p ocomment + - name: Report the environment and the configuration OComment resolved + run: | + set -euo pipefail + ./rust/target/debug/ocomment doctor + ./rust/target/debug/ocomment config explain + # A bare run is the gate: it walks the repository under the ordinary + # hidden-file and size limits and under `.ocomment.toml`, so a new + # explanatory comment that carries no tag fails the build. + - name: OComment checks its own repository + run: ./rust/target/debug/ocomment --format github + - name: Strip every comment out of a copy of the workspace + run: | + set -euo pipefail + rm -rf "${RUNNER_TEMP}/strip" + mkdir -p "${RUNNER_TEMP}/strip" + git ls-files -z rust | xargs -0 cp --parents -t "${RUNNER_TEMP}/strip" + # The copy inherits no configuration of its own, and the patched + # crate under rust/vendor is not ours to rewrite. + printf 'version = 1\n\n[files]\nexclude = ["rust/vendor/**"]\n' \ + >"${RUNNER_TEMP}/strip/.ocomment.toml" + cd "${RUNNER_TEMP}/strip" + "${GITHUB_WORKSPACE}/rust/target/debug/ocomment" \ + fix --policy all --force-protected 2>&1 | tee "${RUNNER_TEMP}/strip.log" + grep -qE 'Removed [0-9]+ comments? in [0-9]+ files?' "${RUNNER_TEMP}/strip.log" + - name: The stripped workspace still builds and still passes the core tests + run: | + set -euo pipefail + cargo build --manifest-path "${RUNNER_TEMP}/strip/rust/Cargo.toml" --workspace --locked + cargo test --manifest-path "${RUNNER_TEMP}/strip/rust/Cargo.toml" -p ocomment-core --locked host-smoke: strategy: @@ -138,3 +197,56 @@ jobs: command -v python3 >/dev/null 2>&1 || python=python "${python}" tools/validate_schemas.py --sarif "${SMOKE_SARIF_FILE}" | tee sarif-report.txt grep -q 'with 2 ocomment results' sarif-report.txt + + docker: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 + # A source build on one platform, which is the path a release never + # takes, so the Dockerfile's own builder stage cannot rot between + # releases. The step after the smoke test takes the release path over the + # same file. + - name: Build the image from source + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 + with: + context: . + platforms: linux/amd64 + push: false + load: true + tags: ocomment:ci + - name: Smoke test the image + shell: bash + run: | + set -euo pipefail + docker run --rm ocomment:ci --version + status=0 + docker run --rm -v "$PWD/spec:/src" ocomment:ci check --format json \ + >container-report.json || status=$? + if [ "$status" -gt 1 ]; then + echo "::error::the image failed to scan the mounted directory (exit ${status})" + exit 1 + fi + python3 -c 'import json, sys; json.load(open(sys.argv[1]))' container-report.json + # The release image is not compiled: the workflow replaces the `builder` + # stage with a buildx named context holding the musl binaries the release + # matrix already built. Handing the image its own binary back through + # that context exercises the second path over the same Dockerfile, so a + # release build is never the first to find the layout broken. The + # `docker/setup-buildx-action` step above is what makes `--build-context` + # available; this step uses that same builder. + - name: Build the image again through the release path + shell: bash + run: | + set -euo pipefail + mkdir -p release/binaries/out/amd64 + container=$(docker create ocomment:ci) + docker cp "${container}:/ocomment" release/binaries/out/amd64/ocomment + docker rm "${container}" + chmod 0755 release/binaries/out/amd64/ocomment + docker buildx build --build-context builder=release/binaries \ + --load -t ocomment:ci-prebuilt . + docker run --rm ocomment:ci-prebuilt --version diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index feaa114..7ec8068 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -186,6 +186,92 @@ jobs: GH_TOKEN: ${{ github.token }} run: gh release create "$GITHUB_REF_NAME" release/* --generate-notes --verify-tag + publish-container: + needs: publish-release + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + actions: read # Download the musl archives the build matrix produced. + attestations: write # Publish build provenance for the pushed image. + contents: read # Read the Dockerfile and the licences out of the tag. + id-token: write # Obtain keyless Sigstore identities for signing. + packages: write # Push the image into this repository's GHCR namespace. + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + pattern: ocomment-*-unknown-linux-musl + path: musl + merge-multiple: true + # The image ships the binaries this release already built, smoke tested, + # signed, and published as archives rather than a second compilation of + # the same tag. `builder` below is the buildx named context the Dockerfile + # copies from, so this layout is the whole contract between them. + - name: Lay the released musl binaries out as the `builder` context + shell: bash + run: | + set -euo pipefail + extract() { + mkdir -p "release/binaries/out/$2" + tar -xzf "musl/ocomment-$1.tar.gz" -C "release/binaries/out/$2" \ + --strip-components=1 "ocomment-$1/ocomment" + chmod 0755 "release/binaries/out/$2/ocomment" + } + extract x86_64-unknown-linux-musl amd64 + extract aarch64-unknown-linux-musl arm64 + # GHCR rejects an uppercase path, and this repository has one. + - name: Resolve the GHCR image name + id: image + shell: bash + run: echo "name=ghcr.io/${GITHUB_REPOSITORY,,}" >>"$GITHUB_OUTPUT" + - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4 + - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 + with: + images: ${{ steps.image.outputs.name }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + labels: | + org.opencontainers.image.title=ocomment + org.opencontainers.image.description=Fast, byte-preserving comment checker and remover + org.opencontainers.image.licenses=MIT OR Apache-2.0 + org.opencontainers.image.documentation=https://github.com/${{ github.repository }}/blob/${{ github.ref_name }}/docs/docker.md + - id: build + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 + with: + context: . + build-contexts: builder=release/binaries + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + annotations: ${{ steps.meta.outputs.annotations }} + provenance: mode=max + sbom: true + - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + - name: Sign the pushed image + shell: bash + env: + DIGEST: ${{ steps.build.outputs.digest }} + IMAGE: ${{ steps.image.outputs.name }} + run: cosign sign --yes "${IMAGE}@${DIGEST}" + - name: Attest build provenance + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4 + with: + subject-name: ${{ steps.image.outputs.name }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true + publish-crates: needs: publish-release runs-on: ubuntu-latest diff --git a/.ocomment.toml b/.ocomment.toml new file mode 100644 index 0000000..5e1517d --- /dev/null +++ b/.ocomment.toml @@ -0,0 +1,20 @@ +# OComment checks its own repository. `ocomment` from the root is the gate the +# `dogfood` CI job runs, and Lefthook runs `ocomment check --staged` before every +# commit; see CONTRIBUTING.md for the tag convention this configuration enforces. +version = 1 + +[files] +exclude = [ + # Vendored crates, fixture bytes, and packaging or benchmark scratch are not + # ours to rewrite: fixture comments are the test input itself. + "rust/vendor/**", + "spec/fixtures/**", + "release-extras/**", + "benchmarks/**", +] + +[policy] +mode = "legal" +layout = "lines" +keep_kind = ["doc-line", "doc-block"] +keep_regex = ['^(//|\(\*|/\*|#)\s*(NOTE|SAFETY|INVARIANT|PERF|TODO|FIXME|HACK)\b'] diff --git a/CHANGELOG.md b/CHANGELOG.md index e977539..6d88d7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,22 @@ All notable changes to OComment will be documented here. The project follows is left with the flag that would overrule it. The machine formats refuse the flag rather than ignoring it, and so does every command that writes no report of comments for it to annotate. +- The repository checks itself. `.ocomment.toml` runs the `legal` policy with + `doc-line` and `doc-block` kept and protects any comment headed `NOTE`, + `SAFETY`, `INVARIANT`, `PERF`, `TODO`, `FIXME`, or `HACK`, so an explanatory + comment that says why it is there survives and one that only restates the + line below it does not. Every such comment in `rust/` and `ocaml/` carries + its tag, as does every one in the Python and shell tooling and in the + `Dockerfile`; the only paths left out of the gate are vendored crates, + fixture bytes, and packaging and benchmark scratch. `SAFETY` is reserved for + its Rust-wide meaning — justifying an `unsafe` block — and a rationale about + bytes or spoofing is an `INVARIANT`. `lefthook.yml` runs `ocomment check + --staged` before each commit, and the `dogfood` CI job runs a bare `ocomment` + over the tree, reports the environment through `doctor` and `config explain`, + and then strips every comment out of a copy of the sources with `fix --policy + all --force-protected` and rebuilds it: the Rust workspace builds and + `ocomment-core` still passes its tests, and the `reference` job does the same + for the OCaml reference. `CONTRIBUTING.md` documents the tags. ### Changed @@ -85,6 +101,20 @@ All notable changes to OComment will be documented here. The project follows ### Fixed +- `--staged` honours `files.include` and `files.exclude`. It read every path + `git diff --cached` named, so a commit that touched an excluded tree — a + vendored crate, generated tooling — was reported by the pre-commit hook, and + `fix --staged` rewrote its index blob. A staged path is a walked path rather + than a named one: it is measured against the project root exactly as a walk + measures one, from whichever directory the command was typed in. +- A Rust string or byte-string literal may carry a bare newline, so a scan no + longer ends one at the end of its line. `ocomment` reported its own + `rust/ocomment/src/cli.rs` as invalid — two `unterminated-string` + diagnostics for a multi-line `&str` constant — and then read the rest of the + literal as source, finding comments inside it and refusing to write anything + for the file. A Rust character literal still ends at the line, which is what + keeps a lifetime from swallowing the rest of the source, and C, C++, Go, and + Java literals are unchanged. The OCaml reference agrees. - A walk never descends into `.git`, whatever lifted the hidden-file rule. Naming a directory does lift it, and so does `files.hidden`, so `ocomment fix .` in a fresh repository used to rewrite the sample hooks git had just @@ -100,6 +130,20 @@ All notable changes to OComment will be documented here. The project follows given no base id has nothing to resolve the path against. An absolute path, a path that climbs out of the tree through `..`, and the `` pseudo-path carry no base id, because none of them is under the source root. +- `# syntax=` and `# hadolint ignore=` are directives. A Dockerfile is scanned + as shell, and both lines are read by a tool rather than by a person: removing + the first changes which Dockerfile frontend builds the image, and removing + the second turns a linter rule back on. The OCaml reference agrees, and the + differential harness carries the case. +- `--format github` folds a walked skip away unless `-v` asks for it, the way + the human report already did. A run over a repository annotated every file it + had no scanner for, so the checks tab filled with notices about Markdown and + YAML. An I/O error and a path the caller named are still always annotated. +- An invalid `[policy]` regex is reported on one line and in full. The `regex` + crate writes a parse error over four lines with a caret under the byte it + stopped at; the report replaced the newlines with U+FFFD instead of folding + them, so a single failure arrived as one unreadable line of replacement + characters. - `tools/release_manifests.py` defaults `--repository` to `P4suta/OComment`. The release workflow passes `$GITHUB_REPOSITORY`, so the old default only ever reached someone generating the definitions by hand — and pointed the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7c4a58..c852608 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,8 +23,17 @@ also requires OCaml 5.5, opam, Dune 3.24.2, Python 3, and the dependencies from ```sh opam install ./ocaml/ocomment-ref.opam --deps-only --with-test cargo build --manifest-path rust/Cargo.toml --workspace --locked +lefthook install ``` +`lefthook install` wires up `lefthook.yml`, whose `pre-commit` hook runs +`ocomment check --staged` and `cargo fmt --check`. The hook reads the staged +blobs rather than the working tree, so a partially staged file is judged by the +bytes the commit will carry, and it reports rather than rewrites: `fix --staged` +under Lefthook would need `stage_fixed`, which stages the whole working-tree +file and destroys partial staging. It prefers an `ocomment` on `PATH` and falls +back to the workspace copy, so a fresh clone needs no `cargo install` first. + The repository is intentionally split into independent implementations: - `spec/` contains shared contracts and fixtures. @@ -49,14 +58,66 @@ python3 tools/check_embedded_specs.py python3 tools/check_hooks.py python3 tools/validate_schemas.py ./tools/package-list.sh +ocomment actionlint +lefthook validate ``` +A bare `ocomment` from the repository root is the gate CI runs; see +[comments carry a tag](#comments-carry-a-tag). + When behavior changes, add the smallest fixture that proves the lexical edge case. Keep byte spans half-open, edits sorted and non-overlapping, and output deterministic. Update both implementations and their differential expectations when the shared contract changes. +## Comments carry a tag + +OComment checks its own repository. `.ocomment.toml` runs the `legal` policy +with `doc-line` and `doc-block` kept, so documentation is never at risk, and it +keeps any comment whose first word is one of these tags: + +| Tag | What it introduces | +| --- | --- | +| `NOTE` | Why the code is the way it is, where the code cannot say so itself. | +| `SAFETY` | Why an `unsafe` block upholds what the compiler cannot check. It is reserved for that Rust-wide meaning; the workspace has no `unsafe` today. | +| `INVARIANT` | A property the surrounding code must preserve for the next reader to be able to change it — why an operation cannot lose a user's bytes or be spoofed included. | +| `PERF` | A measurement or a hot path that explains a shape which would otherwise look convoluted. | +| `TODO` / `FIXME` / `HACK` | Work that is known to be left, in decreasing order of how deliberate it was. | + +An untagged explanatory comment fails the `dogfood` CI job and the pre-commit +hook. The rule is not that comments are unwelcome — it is that a comment worth +keeping is worth saying *why* it is there, and a comment that cannot be given +one of these tags is usually restating what the line below it already says. + +The tag is matched against the head of a single comment token, so a rationale +that runs past one line is one block comment rather than a run of `//` lines: + +```rust +/* INVARIANT: a Rust string literal carries a bare newline as content, unlike + * its C, Go, and Java cousins, so only the closing quote or the end of the + * file ends one. */ +``` + +Keep the continuation lines on ` * `; `rustfmt` reflows the other block-comment +shapes. In OCaml the same rationale is `(* INVARIANT: ... *)`. + +A language whose only comment is `#` — shell, Python, the Dockerfile — has no +block form to run a rationale through, and every `#` line is a comment token of +its own, so every one of them carries the tag: + +```dockerfile +# NOTE: musl-dev is deliberately unpinned: the version that matters is the one +# NOTE: the pinned `rust:1.88-alpine` tag resolves to, and pinning a package +# NOTE: version on top of that only breaks the build when the base image moves. +``` + +Prose that documents a Python object belongs in its docstring instead, which is +not a comment at all. + +`ocomment config explain` names the setting behind each of these rules, and +`ocomment --explain` names the rule that decided any one comment. + ## CLI output conventions Findings, patches, generated files, and every machine format are written to diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cd7a43b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# syntax=docker/dockerfile:1 + +# NOTE: `scratch` plus one statically linked musl binary and the two licences: +# NOTE: no shell, no package manager, no libc. docs/docker.md says what a +# NOTE: caller gives up for that, and how a published image is verified. + +FROM rust:1.88-alpine AS builder +ARG TARGETARCH +# NOTE: musl-dev is deliberately unpinned: the version that matters is the one +# NOTE: the pinned `rust:1.88-alpine` tag resolves to, and pinning a package +# NOTE: version on top of that only breaks the build when the base image moves. +# hadolint ignore=DL3018 +RUN apk add --no-cache musl-dev +WORKDIR /work +COPY . . +# INVARIANT: `docker build .` compiles the CLI here, while the release workflow +# INVARIANT: supplies this whole stage as a buildx named context of binaries it +# INVARIANT: already built, smoke tested, signed, and published as archives. The +# INVARIANT: layout below is the entire contract between the two, which is why +# INVARIANT: the final stage copies from one unchanged line either way and why +# INVARIANT: the released image carries the exact bytes the archives do. +# NOTE: `install -m 0555` because the final stage runs as an unprivileged user +# NOTE: that owns nothing in the image. +RUN cargo build --manifest-path rust/Cargo.toml --release --locked -p ocomment \ + && install -D -m 0555 rust/target/release/ocomment "/out/${TARGETARCH}/ocomment" + +FROM scratch +ARG TARGETARCH +COPY --from=builder /out/${TARGETARCH}/ocomment /ocomment +COPY LICENSE-MIT LICENSE-APACHE /licenses/ +# NOTE: A numeric id needs no /etc/passwd, which a scratch image has no room for. +USER 65532:65532 +# NOTE: The default `check` target is the working directory, so a bare +# NOTE: `docker run -v "$PWD:/src" ` checks whatever was mounted. +WORKDIR /src +ENTRYPOINT ["/ocomment"] +CMD ["check"] diff --git a/README.md b/README.md index c73b161..62c33c4 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,10 @@ ocomment fix src printf '%s\n' 'let x = 1; // remove' | ocomment strip --language rust ``` +Or install nothing: `docker run --rm -v "$PWD:/src" +ghcr.io/p4suta/ocomment:0.1.0 check` runs the same CLI from the +[container image](docs/docker.md). + A command that names no path checks the current directory, so running it from a subdirectory checks that subdirectory; a bare `ocomment` run from the repository root already checks the whole repository, under the ordinary walk @@ -89,9 +93,9 @@ path, or the flag on the command line. A comment no setting decided is left with the flag that would overrule the built-in rule instead. `--explain` annotates a report of comments, so it belongs to the two commands that write one and to the one format with room for prose: asking for it with `--format -json`, or any other machine format, or with `fix`, `diff`, or `strip`, is a -usage error rather than a flag that quietly does nothing, and `-q` silences -`check` altogether, explanations included. +json`, or any other machine format, or with any command that writes no report +of comments, is a usage error rather than a flag that quietly does nothing, +and `-q` silences `check` altogether, explanations included. `check` exits 0 when clean, 1 when removable comments exist, and 2 for an invalid source, configuration, plugin, or I/O failure. `diff` and diff --git a/action.yml b/action.yml index f9b0c5a..407deed 100644 --- a/action.yml +++ b/action.yml @@ -68,7 +68,7 @@ outputs: description: Release tag that was downloaded, or the version the supplied binary reported. value: ${{ steps.resolve.outputs.version }} sarif-file: - description: Absolute path of the SARIF file, empty when format is not sarif. + description: Absolute path of the SARIF file in the runner's own spelling, empty when format is not sarif. value: ${{ steps.run.outputs.sarif-file }} runs: @@ -187,7 +187,15 @@ runs: fi if [ "${extension}" = "zip" ]; then - unzip -q -o "${archive}" + # A zip archive is only built for Windows, and a Windows runner is + # not guaranteed an unzip -- but it is guaranteed a PowerShell, the + # way a macOS runner without sha256sum is guaranteed shasum. + if command -v unzip >/dev/null 2>&1; then + unzip -q -o "${archive}" + else + powershell -NoProfile -NonInteractive -Command \ + "Expand-Archive -LiteralPath '${archive}' -DestinationPath '.' -Force" + fi else tar -xzf "${archive}" fi @@ -265,10 +273,21 @@ runs: code=$? set -e + # The redirection above was bash's, so the path stays bash's until it + # leaves the step. What reads the output is not bash: the upload step + # is a Node action, and a caller's own step may be pwsh. On Windows a + # `/d/a/repo/ocomment.sarif` names nothing to either of them, so the + # value is handed over as `D:\a\repo\ocomment.sarif` wherever cygpath + # is there to say so, and unchanged everywhere else. + sarif_output="${sarif_file}" + if [ -n "${sarif_output}" ] && command -v cygpath >/dev/null 2>&1; then + sarif_output="$(cygpath -w "${sarif_file}")" + fi + echo "ocomment exited ${code}" { echo "exit-code=${code}" - echo "sarif-file=${sarif_file}" + echo "sarif-file=${sarif_output}" } >>"${GITHUB_OUTPUT}" - name: Upload SARIF results diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000..932f45a --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,137 @@ +# The container image + +Every release publishes a multi-architecture image to the GitHub Container +Registry: + +```sh +docker run --rm -v "$PWD:/src" ghcr.io/p4suta/ocomment:0.1.0 check +``` + +`linux/amd64` and `linux/arm64` are built, and both carry the exact binary the +matching `ocomment--unknown-linux-musl.tar.gz` release archive contains +— the release workflow pushes the artifacts it already built and smoke tested +rather than compiling the tag a second time. + +## What is in the image + +A `scratch` base, one statically linked musl binary at `/ocomment`, and +`LICENSE-MIT` and `LICENSE-APACHE` under `/licenses`. There is no shell, no +package manager, and no libc, so nothing else can be run in the container and +`docker exec … sh` has nothing to exec. The entrypoint is the binary itself, +which is why arguments are written as if `ocomment` were on the command line: + +```sh +docker run --rm -v "$PWD:/src" ghcr.io/p4suta/ocomment:0.1.0 --version +docker run --rm -v "$PWD:/src" ghcr.io/p4suta/ocomment:0.1.0 diff src >fix.patch +docker run --rm -v "$PWD:/src" ghcr.io/p4suta/ocomment:0.1.0 check --format sarif +``` + +The working directory is `/src` and the default command is `check`, so a bare +run checks whatever was mounted there. Exit codes, `--format`, and the +`.ocomment.toml` discovery rules are the ones +[the CLI documents](../README.md): a config file inside the mounted tree is +found exactly as it would be on the host. + +## Writing files back + +The container runs as uid 65532, which owns nothing on the host, so `fix` needs +to be told who to write as: + +```sh +docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/src" \ + ghcr.io/p4suta/ocomment:0.1.0 fix src +``` + +`fix` writes each file through a temporary file beside it, so the process needs +write permission on the containing directory as well as the file. For a +read-only command, mounting read-only makes that explicit and costs nothing: + +```sh +docker run --rm -v "$PWD:/src:ro" ghcr.io/p4suta/ocomment:0.1.0 check +``` + +`fix --interactive` needs a terminal on both standard input and standard +output, so add `-it` when you want it. + +## What the image cannot do + +`--staged` reads and rewrites Git index blobs by running `git`, and there is no +`git` in the image, so it fails with `--staged needs a Git repository`. +Mounting the host's `.git` directory does not help — the binary still has no +`git` to run. Use the host CLI for staged workflows: `cargo install ocomment +--locked`, a release archive, or the [pre-commit hook](ci.md). + +Fetching a plugin from an `https:`, `gh:`, or `oci:` source needs `curl`, `gh`, +or `oras`, and `--identity` verification needs `cosign`; none of them are in +the image either. A plugin already vendored into the mounted tree loads +normally, because that is the binary's own WASM host doing the work. `ocomment +doctor` lists every one of these: + +```console +$ docker run --rm -v "$PWD:/src:ro" ghcr.io/p4suta/ocomment:0.1.0 doctor +... +git: not found (needed for --staged) +curl: not found (needed for https:// plugin sources) +gh: not found (needed for gh: plugin sources) +oras: not found (needed for oci: plugin sources) +cosign: not found (needed for --identity verification) +``` + +## Tags + +`0.1.0` pins one release. `0.1` follows the patch releases of that minor +series, and `latest` follows the newest release. Pin the full version in CI, +or pin the digest when the image must never move at all: + +```sh +docker run --rm -v "$PWD:/src" ghcr.io/p4suta/ocomment@sha256:… check +``` + +## Verifying the image + +The image is signed keylessly with Sigstore and carries a build-provenance +attestation, both bound to the release workflow of this repository: + +```sh +cosign verify ghcr.io/p4suta/ocomment:0.1.0 \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity-regexp '^https://github\.com/P4suta/OComment/\.github/workflows/release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' +``` + +The regexp is the point of the check: it accepts only a signature made by +`.github/workflows/release.yml` in `P4suta/OComment` running on a `v*` tag. +A looser identity — anything matching `.*`, say — would accept a signature +from any workflow in any repository and prove nothing. + +The provenance attestation is verified with either the GitHub CLI or cosign: + +```sh +gh attestation verify oci://ghcr.io/p4suta/ocomment:0.1.0 --repo P4suta/OComment +``` + +The image is also published with an SPDX SBOM and SLSA provenance attached by +buildx, which `docker buildx imagetools inspect ghcr.io/p4suta/ocomment:0.1.0` +lists. + +## Building it yourself + +`docker build .` at the repository root compiles the CLI from source in a +`rust:1.88-alpine` stage instead of downloading a release. That is the path CI +exercises on every pull request, so it stays working between releases: + +```sh +docker build -t ocomment:dev . +docker run --rm -v "$PWD:/src:ro" ocomment:dev check +``` + +A source build compiles for the platform being built, so building a foreign +architecture this way runs the compiler under emulation and is slow. The +release workflow avoids that entirely by replacing the builder stage with a +buildx named context holding the already-built binaries: + +```sh +docker buildx build --build-context builder=release/binaries \ + --platform linux/amd64,linux/arm64 . +``` + +where `release/binaries` holds `out/amd64/ocomment` and `out/arm64/ocomment`. diff --git a/docs/releasing.md b/docs/releasing.md index 6eed140..da40ab6 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -32,6 +32,28 @@ upstream submission is pending. The CLI crate contains explicit `cargo-binstall` URL, archive-format, and in-archive binary metadata for the same target-qualified archives. +## The container image + +`publish-container` pushes `ghcr.io/p4suta/ocomment` for `linux/amd64` and +`linux/arm64` after the GitHub release exists. It does not rebuild the tag: it +downloads the two `*-unknown-linux-musl` archives the matrix already produced +and feeds them to the Dockerfile through the buildx named context `builder`, so +the image and the archives contain the same bytes. The image is tagged +`MAJOR.MINOR.PATCH`, `MAJOR.MINOR`, and `latest`, signed with cosign, and given +a build-provenance attestation pushed to the registry. + +A GHCR package is private when it is first created, and the visibility setting +belongs to the package rather than the repository, so nothing in this +repository can set it. **After the first release, open the package page and set +its visibility to public once.** Until that is done every `docker pull` fails +with an authentication error even though the workflow succeeded. Later releases +inherit the setting. + +Renaming the image, dropping an architecture, or moving the `builder` context +layout — `out/amd64/ocomment` and `out/arm64/ocomment` — breaks pinned pulls +and the Dockerfile respectively, so treat both as part of the released +contract, exactly like the archive layout. + ## The published GitHub Action `action.yml` at the repository root is released with the source, so diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..f93255a --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,26 @@ +# Git hooks for this repository. Install them once with `lefthook install`. +# +# `ocomment check --staged` reads the staged blobs rather than the working tree, +# so a partially staged file is judged by the bytes the commit will actually +# carry. It reports rather than rewrites: `fix --staged` would need Lefthook's +# `stage_fixed`, and that setting stages the whole working-tree file and +# destroys partial staging. +pre-commit: + parallel: true + commands: + ocomment: + # Prefer an installed CLI; fall back to the workspace copy so a fresh + # clone gets the same gate without `cargo install` first. + run: | + if command -v ocomment >/dev/null 2>&1; then + ocomment check --staged + else + cargo run --quiet --manifest-path rust/Cargo.toml --locked -p ocomment -- check --staged + fi + rustfmt: + glob: "rust/**/*.rs" + run: cargo fmt --all --manifest-path rust/Cargo.toml -- --check + + skip: + - merge + - rebase diff --git a/ocaml/lib/ocomment_ref.ml b/ocaml/lib/ocomment_ref.ml index dbe5a35..4bf41ca 100644 --- a/ocaml/lib/ocomment_ref.ml +++ b/ocaml/lib/ocomment_ref.ml @@ -225,7 +225,8 @@ let is_directive language text raw = | C | Cpp -> String.starts_with ~prefix:"pragma" compact || String.starts_with ~prefix:"line " compact | Python -> List.exists (fun prefix -> String.starts_with ~prefix compact) ["pyright:"; "mypy:"; "ruff:"; "fmt:"] - | Shell -> String.starts_with ~prefix:"shellcheck" compact + | Shell -> List.exists (fun prefix -> String.starts_with ~prefix compact) + ["shellcheck"; "syntax="; "hadolint"] | _ -> false let within_first_two_lines source finish = @@ -560,7 +561,11 @@ let scan_slash_unmapped source language options accumulator = end else if language = Kotlin && character = '"' then begin loop (scan_kotlin_string source options accumulator index false 0) end else if character = '"' || character = '\'' then begin - let finish, closed = quoted_end source index (language = Css) in + (* INVARIANT: a Rust string or byte-string literal carries a bare + newline as content, so only its closing quote or the end of the + file ends one; a Rust character literal still ends at the line. *) + let multiline = language = Css || (language = Rust && character = '"') in + let finish, closed = quoted_end source index multiline in if not closed then add_error accumulator "unterminated-string" "unterminated literal" index finish; loop finish end else loop (index + 1) @@ -1677,10 +1682,10 @@ let prepended_concatenation_mark value = value = 0x0890 || value = 0x0891 || value = 0x08e2 || value = 0x110bd || value = 0x110cd -(* Keep this character-level policy aligned with unicode-width's documented - Unicode 17 rules. Uucp's tty_width_hint deliberately follows the much - simpler historical wcwidth heuristic and, in particular, gives decomposed - Hangul vowel/trailing jamo a width of one. *) +(* INVARIANT: Keep this character-level policy aligned with unicode-width's + documented Unicode 17 rules. Uucp's tty_width_hint deliberately follows the + much simpler historical wcwidth heuristic and, in particular, gives + decomposed Hangul vowel/trailing jamo a width of one. *) let unicode_width value = let character = Uchar.of_int value in if value < 0x20 || (value >= 0x7f && value < 0xa0) then 0 @@ -1751,7 +1756,7 @@ let replacement source layout kind span = not (ascii_whitespace (Bytes.get source span.finish)) then Bytes.of_string " " else Bytes.empty -(* +(* PERF: Column state is threaded between edits so every source byte is inspected at most once. This also reflects an explicitly removed HTML comment: because that edit emits no bytes, its original newlines do not affect later edits. diff --git a/ocaml/test/test_core.ml b/ocaml/test/test_core.ml index e8b1308..0d04bd5 100644 --- a/ocaml/test/test_core.ml +++ b/ocaml/test/test_core.ml @@ -15,10 +15,17 @@ let check_string () = let report = scan (Bytes.of_string "\"// no\" // yes") JavaScript default_scan_options in Alcotest.(check int) "only real comment" 1 (List.length report.comments) +let check_rust_multiline_string () = + let report = + scan (Bytes.of_string "let s = \"a\n// no\nb\"; // yes") Rust default_scan_options in + Alcotest.(check bool) "valid" true report.valid; + Alcotest.(check int) "only real comment" 1 (List.length report.comments) + let () = Alcotest.run "ocomment-ref" [ "core", [ Alcotest.test_case "transform" `Quick check_transform; Alcotest.test_case "nested" `Quick check_nested; Alcotest.test_case "string" `Quick check_string; + Alcotest.test_case "rust-multiline-string" `Quick check_rust_multiline_string; ] ] diff --git a/rust/ocomment-core/examples/throughput.rs b/rust/ocomment-core/examples/throughput.rs index 3a0c9d0..f201860 100644 --- a/rust/ocomment-core/examples/throughput.rs +++ b/rust/ocomment-core/examples/throughput.rs @@ -54,9 +54,9 @@ fn run() -> Result<(), String> { Dialect::Standard, ), }; - // Keep comment allocation realistic: one span per 4 KiB rather than one - // span per source line. The filler still exercises each language's string - // and other lexically sensitive states. + /* PERF: Keep comment allocation realistic: one span per 4 KiB rather than one + * span per source line. The filler still exercises each language's string + * and other lexically sensitive states. */ let mut fragment = Vec::with_capacity(4096 + filler.len()); while fragment.len() + filler.len() + comment.len() <= 4096 { fragment.extend_from_slice(filler); diff --git a/rust/ocomment-core/src/incremental.rs b/rust/ocomment-core/src/incremental.rs index 566938e..d208e2b 100644 --- a/rust/ocomment-core/src/incremental.rs +++ b/rust/ocomment-core/src/incremental.rs @@ -146,13 +146,13 @@ impl IncrementalDocument { let safe_start = if !can_reuse { 0 } else { - // The checkpoints belong to the *previous* revision, and a - // checkpoint is only a restart point while the bytes around it - // still allow one: an edit that turns line 2 into a Python encoding - // declaration, or that splices two C lines together, withdraws that - // permission. Every candidate is therefore re-asked against the - // edited document, falling back to an earlier checkpoint and - // ultimately to a full scan. + /* INVARIANT: The checkpoints belong to the *previous* revision, and a + * checkpoint is only a restart point while the bytes around it + * still allow one: an edit that turns line 2 into a Python encoding + * declaration, or that splices two C lines together, withdraws that + * permission. Every candidate is therefore re-asked against the + * edited document, falling back to an earlier checkpoint and + * ultimately to a full scan. */ let rules = RestartRules::of(&next, self.language); let usable = self .safe_checkpoints @@ -165,12 +165,12 @@ impl IncrementalDocument { .unwrap_or(0) }; let old_convergence = if can_reuse { - // Converging keeps the previous revision's report for every byte - // past the convergence point, shifted by the edit's length delta — - // including each comment's kind. Only the preamble rules care where - // a comment sits, so the tail may be reused exactly while it lies - // past the preamble both where it was and where the edit moves it; - // otherwise the scan runs on to the first checkpoint that does. + /* INVARIANT: Converging keeps the previous revision's report for every byte + * past the convergence point, shifted by the edit's length delta — + * including each comment's kind. Only the preamble rules care where + * a comment sits, so the tail may be reused exactly while it lies + * past the preamble both where it was and where the edit moves it; + * otherwise the scan runs on to the first checkpoint that does. */ self.safe_checkpoints.iter().copied().find(|point| { *point >= old_tail_start.max(safe_start) && preamble_is_settled(&self.source, *point) @@ -183,10 +183,10 @@ impl IncrementalDocument { let mut partial = None; if let Some(old_convergence) = old_convergence { let new_convergence = new_tail_start + old_convergence - old_tail_start; - // The scanner is handed the whole suffix, never a slice cut at the - // convergence point: lexical lookahead that reaches past the cut - // would otherwise decide differently than it does in the real - // document and the rescan would lose comments or diagnostics. + /* INVARIANT: The scanner is handed the whole suffix, never a slice cut at the + * convergence point: lexical lookahead that reaches past the cut + * would otherwise decide differently than it does in the real + * document and the rescan would lose comments or diagnostics. */ let (report, checkpoints, converged) = scan_until_checkpoint( &next[safe_start..], self.language, @@ -198,8 +198,8 @@ impl IncrementalDocument { reused_tail = Some((old_convergence, new_convergence)); partial = Some((report, checkpoints, new_convergence)); } else { - // Lexical state diverged, so the scan already ran to the end of - // the suffix; that report is exactly the fallback. + /* NOTE: Lexical state diverged, so the scan already ran to the end of + * the suffix; that report is exactly the fallback. */ partial = Some((report, checkpoints, next.len())); } } @@ -484,8 +484,8 @@ mod tests { } proptest! { - // Unit-test proptests cannot persist regressions next to `src`, so the - // shrunk counterexample is reported inline instead. + /* NOTE: Unit-test proptests cannot persist regressions next to `src`, so the + * shrunk counterexample is reported inline instead. */ #![proptest_config(ProptestConfig { failure_persistence: None, ..ProptestConfig::default() })] /// Every safe checkpoint must be a restart point: scanning the suffix diff --git a/rust/ocomment-core/src/scanner.rs b/rust/ocomment-core/src/scanner.rs index 22765c2..cc9cb1a 100644 --- a/rust/ocomment-core/src/scanner.rs +++ b/rust/ocomment-core/src/scanner.rs @@ -233,10 +233,10 @@ impl<'a> Scanner<'a> { } fn scan_c_family(&mut self) { - // Translation phase 2 line splicing is significant to C-family lexical - // input. The remapped copy is scanned by a child, which tracks no - // checkpoints — that is the document-wide half of the restart rules, and - // it is the same answer the incremental engine gets from them. + /* INVARIANT: Translation phase 2 line splicing is significant to C-family lexical + * input. The remapped copy is scanned by a child, which tracks no + * checkpoints — that is the document-wide half of the restart rules, and + * it is the same answer the incremental engine gets from them. */ if !self.restart_rules.splicing_permits_restarts { let mapped = MappedBytes::without_c_line_splices(self.source); let mut child = Scanner::child(&mapped.bytes, self.language, self.options.clone(), 0); @@ -316,7 +316,13 @@ impl<'a> Scanner<'a> { } else { index }; - return Some(self.quoted_or_error(quote, false, "string")); + /* INVARIANT: a Rust string or byte-string literal carries a + * bare newline as content, unlike its C, Go, and Java + * cousins, so only the closing quote or the end of the file + * ends one. A character literal below still ends at the + * line, which is what keeps a lifetime from swallowing the + * rest of the source. */ + return Some(self.quoted_or_error(quote, true, "string")); } if bytes[index] == b'\'' && rust_char_start(bytes, index) { return Some(self.quoted_or_error(index, false, "character literal")); @@ -1299,9 +1305,9 @@ impl<'a> Scanner<'a> { _ => {} } } - // Annex B HTML-like comments are uncommon. Guard both delimiter - // checks by their first byte so the ordinary JavaScript hot path - // does not perform two slice comparisons for every source byte. + /* PERF: Annex B HTML-like comments are uncommon. Guard both delimiter + * checks by their first byte so the ordinary JavaScript hot path + * does not perform two slice comparisons for every source byte. */ if (bytes[index] == b'<' && starts(bytes, index, b" + +# Commands + +Each block below is the exact `--help` text of the binary this page was +generated from, so a flag documented here is a flag the tool has, and a flag +that is missing here does not exist. + +`ocomment` with no command is `ocomment check`, and a command with no path is +the current directory. Findings, patches, listings, and every machine format go +to standard output; the run summary and every note go to standard error, so +`ocomment diff src > fix.patch` keeps the patch clean. + +`check` exits `0` when nothing removable was found, `1` when removable comments +were reported or a diff was printed, and `2` for an invalid source, +configuration, plugin, or I/O failure. + +The `Policy` and `Output` option groups are global. Every command that can act +on them accepts them, which is why the same two groups appear under most of the +blocks below. `ocomment man` renders the same material as a manual page. + +## Contents + +- [`ocomment`](#ocomment) +- [`ocomment check`](#ocomment-check) +- [`ocomment fix`](#ocomment-fix) +- [`ocomment diff`](#ocomment-diff) +- [`ocomment scan`](#ocomment-scan) +- [`ocomment strip`](#ocomment-strip) +- [`ocomment lsp`](#ocomment-lsp) +- [`ocomment init`](#ocomment-init) +- [`ocomment config`](#ocomment-config) +- [`ocomment languages`](#ocomment-languages) +- [`ocomment plugin`](#ocomment-plugin) +- [`ocomment plugin add`](#ocomment-plugin-add) +- [`ocomment plugin remove`](#ocomment-plugin-remove) +- [`ocomment plugin list`](#ocomment-plugin-list) +- [`ocomment plugin update`](#ocomment-plugin-update) +- [`ocomment plugin verify`](#ocomment-plugin-verify) +- [`ocomment plugin new`](#ocomment-plugin-new) +- [`ocomment completions`](#ocomment-completions) +- [`ocomment doctor`](#ocomment-doctor) +- [`ocomment man`](#ocomment-man) + +## `ocomment` + +```console +$ ocomment --help +OComment scans source bytes without requiring UTF-8 and reports or removes comment tokens. The default policy protects source preambles and tool or language directives. Rewrites are prepared and committed as one rollback-backed transaction. + +Usage: ocomment [OPTIONS] [PATH]... + ocomment + +Commands: + check Report removable comments (default command) + fix Remove comments in place through an atomic, rollback-backed transaction + diff Print a unified diff of the changes fix would make + scan List every comment with its kind, disposition and byte span + strip Read source on stdin and write the stripped result to stdout + lsp Run the LSP 3.18 server over stdio + init Write a starter .ocomment.toml or Lefthook configuration + config Show, locate, explain, or export the resolved configuration + languages List built-in languages, extensions, and dialects + plugin Manage sandboxed WASM scanner plugins + completions Generate shell completions + doctor Diagnose the environment (config, git, plugins, tools) + man Render the roff manual page to stdout + help Print this message or the help of the given subcommand(s) + +Arguments: + [PATH]... + Files or directories to check; `-` reads standard input (default: current directory) + +Options: + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file + +EXIT STATUS + 0 Nothing removable was found and every requested change was applied. + 1 Removable comments were reported, or a diff was printed. + 2 Invalid source, configuration, plugin, or I/O failure. + +FILES + .ocomment.toml Project configuration, merged over the user file. + .ocommentignore Extra ignore patterns honoured by repository walks. + .ocomment.lock Pinned digests of the installed WASM scanner plugins. + $XDG_CONFIG_HOME/ocomment/config.toml + User configuration, merged over the built-in defaults. + +EXAMPLES + ocomment + Check the current directory and report removable comments. + ocomment fix --policy all --layout compact src + Remove every comment under src and close the gaps it leaves. + ocomment strip --language rust < before.rs > after.rs + Strip one file from standard input to standard output. + +SEE ALSO + The complete schemas and guides are available in the OComment repository. +``` + +## `ocomment check` + +```console +$ ocomment check --help +Report removable comments (default command) + +Usage: ocomment check [OPTIONS] [PATH]... + +Arguments: + [PATH]... + Files or directories to process; `-` reads standard input (default: current directory) + +Options: + --staged + Read and update Git index blobs rather than treating the working tree as the source + + --index-only + With `--staged`, do not attempt a uniquely mappable working-tree update + + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment fix` + +```console +$ ocomment fix --help +Remove comments in place through an atomic, rollback-backed transaction + +Usage: ocomment fix [OPTIONS] [PATH]... + +Arguments: + [PATH]... + Files or directories to rewrite (default: current directory) + +Options: + --staged + Read and update Git index blobs rather than treating the working tree as the source + + --index-only + With `--staged`, do not attempt a uniquely mappable working-tree update + + --dry-run + Print the patch `fix` would apply and write nothing + + -i, --interactive + Ask about each comment in turn and remove only the accepted ones. + + The index has no working-tree line to show a hunk from, `--dry-run` writes nothing whatever the answers were, and `-q` asks for a run with no commentary at all. None of the three can also be a conversation. + + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment diff` + +```console +$ ocomment diff --help +Print a unified diff of the changes fix would make + +Usage: ocomment diff [OPTIONS] [PATH]... + +Arguments: + [PATH]... + Files or directories to process; `-` reads standard input (default: current directory) + +Options: + --staged + Read and update Git index blobs rather than treating the working tree as the source + + --index-only + With `--staged`, do not attempt a uniquely mappable working-tree update + + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment scan` + +```console +$ ocomment scan --help +List every comment with its kind, disposition and byte span + +Usage: ocomment scan [OPTIONS] [PATH]... + +Arguments: + [PATH]... + Files or directories to process; `-` reads standard input (default: current directory) + +Options: + --staged + Read and update Git index blobs rather than treating the working tree as the source + + --index-only + With `--staged`, do not attempt a uniquely mappable working-tree update + + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment strip` + +```console +$ ocomment strip --help +Read source on stdin and write the stripped result to stdout + +Usage: ocomment strip [OPTIONS] + +Options: + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment lsp` + +```console +$ ocomment lsp --help +Run the LSP 3.18 server over stdio + +Usage: ocomment lsp [OPTIONS] + +Options: + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment init` + +```console +$ ocomment init --help +Write a starter .ocomment.toml or Lefthook configuration + +Usage: ocomment init [OPTIONS] [KIND] + +Arguments: + [KIND] + Which starter file to write + + [default: config] + [possible values: config, lefthook] + +Options: + --fix + For the Lefthook hook, run `fix` instead of `check` + + --force + Replace the file if it already exists + + --stdout + Print the template to standard output and write no file + + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment config` + +```console +$ ocomment config --help +Show, locate, explain, or export the resolved configuration + +Usage: ocomment config [OPTIONS] [ACTION] + +Arguments: + [ACTION] + Which view of the resolved configuration to print + + [default: show] + [possible values: show, locate, explain, schema] + +Options: + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment languages` + +```console +$ ocomment languages --help +List built-in languages, extensions, and dialects + +Usage: ocomment languages [OPTIONS] + +Options: + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment plugin` + +```console +$ ocomment plugin --help +Manage sandboxed WASM scanner plugins + +Usage: ocomment plugin [OPTIONS] + +Commands: + add Install a plugin and pin its digest in .ocomment.lock + remove Uninstall a plugin and drop its lock entry + list List the installed plugins and their pinned digests + update Re-fetch plugins and refresh their pinned digests + verify Check installed plugins against their pinned digests + new Scaffold a new plugin crate from the scanner WIT world + help Print this message or the help of the given subcommand(s) + +Options: + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment plugin add` + +```console +$ ocomment plugin add --help +Install a plugin and pin its digest in .ocomment.lock + +Usage: ocomment plugin add [OPTIONS] + +Arguments: + + Path or URL of the WASM component to install + +Options: + --name + Name to register the plugin under (default: the file stem) + + --sha256 + Expected SHA-256 digest of the component, verified before install + + --identity + Publisher identity recorded alongside the pinned digest + + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment plugin remove` + +```console +$ ocomment plugin remove --help +Uninstall a plugin and drop its lock entry + +Usage: ocomment plugin remove [OPTIONS] + +Arguments: + + Name of the plugin to remove + +Options: + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment plugin list` + +```console +$ ocomment plugin list --help +List the installed plugins and their pinned digests + +Usage: ocomment plugin list [OPTIONS] + +Options: + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment plugin update` + +```console +$ ocomment plugin update --help +Re-fetch plugins and refresh their pinned digests + +Usage: ocomment plugin update [OPTIONS] [NAME] + +Arguments: + [NAME] + Name of the plugin to update (default: all of them) + +Options: + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment plugin verify` + +```console +$ ocomment plugin verify --help +Check installed plugins against their pinned digests + +Usage: ocomment plugin verify [OPTIONS] [NAME] + +Arguments: + [NAME] + Name of the plugin to verify (default: all of them) + +Options: + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment plugin new` + +```console +$ ocomment plugin new --help +Scaffold a new plugin crate from the scanner WIT world + +Usage: ocomment plugin new [OPTIONS] + +Arguments: + + Directory to create the plugin crate in + +Options: + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment completions` + +```console +$ ocomment completions --help +Generate shell completions + +Usage: ocomment completions [OPTIONS] + +Arguments: + + Shell whose completion script is written to stdout + + [possible values: bash, elvish, fish, powershell, zsh] + +Options: + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment doctor` + +```console +$ ocomment doctor --help +Diagnose the environment (config, git, plugins, tools) + +Usage: ocomment doctor [OPTIONS] + +Options: + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` + +## `ocomment man` + +```console +$ ocomment man --help +Render the roff manual page to stdout + +Usage: ocomment man [OPTIONS] + +Options: + --config + Read this configuration file instead of discovering `.ocomment.toml` + + -h, --help + Print help (see a summary with '-h') + +Policy: + --policy + Which classes of comment the run is allowed to remove + + Possible values: + - safe: Remove ordinary and doc comments; keep preambles and directives + - legal: Like safe, and keep licence and copyright comments as well + - all: Remove every comment that no keep override protects + + --layout + How the bytes left behind by a removed comment are laid out + + Possible values: + - lines: Keep the line structure and separate tokens that would otherwise join + - columns: Pad each removed comment so the following columns do not shift + - compact: Drop the comment bytes and keep only the newlines it spanned + + --language + Force this language instead of detecting it from path and contents + + Possible values: + - rust: Rust source files + - ocaml: OCaml implementation and interface files + - c: C source and header files + - cpp: C++ source and header files + - go: Go source files + - java: Java source files, including Unicode escape translation + - javascript: JavaScript modules and scripts, including JSX + - typescript: TypeScript modules and scripts, including TSX + - python: Python source and stub files + - shell: POSIX sh, Bash, and zsh scripts + - html: HTML documents, including nested script and style elements + - css: CSS stylesheets + - jsonc: JSON with comments, including JSON5 + - sql: SQL for every supported database dialect + - kotlin: Kotlin source and script files + + --dialect + Force this dialect of the selected language + + Possible values: + - standard: The default lexical rules of the language + - jsx: JavaScript with JSX elements + - tsx: TypeScript with JSX elements + - objective-c: Objective-C extensions to C + - objective-cpp: Objective-C++ extensions to C++ + - gnu-c: GNU extensions to C + - gnu-cpp: GNU extensions to C++ + - cuda: CUDA extensions to C++ + - posix-sh: The POSIX shell command language + - bash53: Bash 5.3 + - zsh: The Z shell + - postgresql: PostgreSQL, with dollar-quoted bodies + - mysql: MySQL, including its executable versioned comments + - sqlite: SQLite + - t-sql: Microsoft Transact-SQL + - oracle: Oracle SQL and PL/SQL + + --keep-kind + Comma-separated comment kinds to protect on top of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --remove-kind + Comma-separated comment kinds to remove regardless of the policy + + Possible values: + - line: An ordinary comment running to the end of the line + - block: An ordinary delimited comment + - doc-line: A documentation comment running to the end of the line + - doc-block: A delimited documentation comment + - directive: A tool or language directive such as a pragma or lint control + - license: A licence or copyright preamble + - html-comment: A DOM-observable HTML comment + - shebang: The interpreter line starting an executable script + - encoding: A source encoding declaration + - optimizer-hint: A compiler or database optimizer hint + - version-comment: A MySQL versioned comment that the server executes + + --force-invalid + Apply the edits that are still provably safe when the source fails to scan + + --force-protected + Remove protected comments such as shebang and encoding preambles + +Output: + --format + Output encoding + + [default: human] + [possible values: human, json, jsonl, sarif, github] + + --color + When to colour terminal output + + [default: auto] + [possible values: auto, always, never] + + --hyperlinks + When to emit terminal hyperlinks for reported paths + + [default: auto] + [possible values: auto, always, never] + + --no-preview + Omit the one-line comment text from human `check` and `scan` lines + + --explain + List every comment human `check` and `scan` met and name the rule and setting behind each one + + --progress + When to draw the live scanning counter on standard error + + [default: auto] + [possible values: auto, always, never] + + -q, --quiet + Drop the run summary and notes; the command's product (findings, patch, listing) is still written + + -v, --verbose + Trace what is scanned and summarize every comment kind and skipped file +``` diff --git a/docs/comparison.md b/docs/comparison.md new file mode 100644 index 0000000..118fc13 --- /dev/null +++ b/docs/comparison.md @@ -0,0 +1,61 @@ +# Comparison + +Several well-known tools remove comments from source code, and most of them were +built for a different job than OComment was. This page is about scope, not +quality: each of these does its own job well, and the useful question is which +job you have. + +> The rows below describe each project's **documented purpose and scope**, taken +> from its own documentation, and were not benchmarked or feature-tested here. +> Tools change; check the current documentation of anything on this page before +> relying on a row. Only the OComment column describes software this book can +> check: the rest is read off other projects' own documentation. + +| | OComment | `strip-comments` | `decomment` | `cloc --strip-comments` | `gcc -E -fpreprocessed` | +| --- | --- | --- | --- | --- | --- | +| What it is | A comment checker and remover | A Node.js library and CLI for stripping comments | A Node.js library for stripping comments | A line counter, with a comment-stripping side output | A C preprocessor | +| Built to | Report, gate, and remove comments under a policy | Strip comments from JavaScript-style source | Strip comments while preserving string literals | Count lines of code | Preprocess C-family translation units | +| Language coverage | [15 languages and 16 dialects](languages.md), plus declarative profiles and WebAssembly plugins | JavaScript and other C-style syntaxes | JavaScript, JSON, CSS, HTML | Very broad, from its own per-language comment table | C, C++, Objective-C, and their preprocessed inputs | +| Keeps tool directives by default | Yes — shebangs, encoding preambles, `//go:build`, lint controls, optimiser hints, MySQL versioned comments | Documents an option for keeping `/*!` "protected" comments | Documents an option for keeping `/*!` "protected" comments | Not a stated goal | Not a stated goal | +| Configurable per path | Yes, `[[overrides]]` globs in `.ocomment.toml` | Through the calling program | Through the calling program | No | No | +| Check-only mode with a CI exit code | Yes, `ocomment check` | No | No | No | No | +| Machine-readable report | JSON, JSONL, SARIF, GitHub annotations | No | No | Counts, in several formats | No | +| Rewrites files in place | Yes, as one rollback-backed transaction | Through the calling program | Through the calling program | Writes a stripped copy of each file | Writes to standard output | +| Non-UTF-8 input | Scanned and preserved byte for byte | Not stated | Not stated | Not stated | Set by `-finput-charset` | +| Keeps line numbers | Yes, every layout | Not stated | Not stated | Not stated | Rewrites line structure, and emits line markers | +| Editor integration | LSP 3.18 server, VS Code extension | No | No | No | Not applicable | +| Independent cross-check | An OCaml reference implementation compared on every fixture | — | — | — | — | +| Installs as | A single static binary, or a crate | An npm package | An npm package | A Perl script or package | Part of a C toolchain | + +## When something else is the right tool + +- **You want a count, not a rewrite.** `cloc` answers "how much of this is + comment?" directly, across more languages than any comment remover needs to + support, and writing the stripped copies is a side output of that. +- **You are already inside a Node build step**, transforming strings in memory + rather than files on disk. A library you can call is less friction than a + binary you have to install, and `strip-comments` and `decomment` are libraries + first. +- **You are preprocessing C anyway.** If the compiler is already running over + the translation unit, `gcc -E` has removed the comments as part of the job. + Note that it is doing much more than that — macro expansion, includes, line + markers — so its output is not the same file minus comments. + +## What OComment adds + +- **A policy, not a switch.** A comment that another program reads is not + commentary, and the default keeps every one it recognises. See + [Why was this comment kept?](why-kept.md). +- **An answer to "why".** `--explain` names the rule and the setting behind + every decision, which is what makes a house rule reviewable rather than + mysterious. +- **A gate.** `ocomment check` exits `1` on findings and speaks SARIF, so the + same tool that removes comments can hold a line in CI and in a pre-commit + hook. This repository uses it on itself. +- **Bytes in, bytes out.** BOMs, CRLF, missing trailing newlines, and non-UTF-8 + bytes outside the edited spans survive a rewrite, and every removal is + committed as one transaction. +- **A second implementation.** The OCaml reference implementation shares no code + with the Rust one, and the two are compared on the scanner, the + classification, the diagnostics, the edits, the transformed bytes, and the + source maps. diff --git a/docs/docker.md b/docs/docker.md index 932f45a..15838de 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -29,7 +29,7 @@ docker run --rm -v "$PWD:/src" ghcr.io/p4suta/ocomment:0.1.0 check --format sari The working directory is `/src` and the default command is `check`, so a bare run checks whatever was mounted there. Exit codes, `--format`, and the `.ocomment.toml` discovery rules are the ones -[the CLI documents](../README.md): a config file inside the mounted tree is +[the CLI documents](configuration.md): a config file inside the mounted tree is found exactly as it would be on the host. ## Writing files back diff --git a/docs/editors.md b/docs/editors.md index b1ef9b6..3d238e9 100644 --- a/docs/editors.md +++ b/docs/editors.md @@ -85,7 +85,7 @@ the open files. `[lsp].on_save = true` in `.ocomment.toml` does the same thing for everyone working in the repository, rather than for one editor. The source is under -[`editors/vscode`](../editors/vscode/README.md). +[`editors/vscode`](https://github.com/P4suta/OComment/blob/main/editors/vscode/README.md). The extension is disabled in untrusted workspaces, because `ocomment.path` names an executable it launches. Run `ocomment doctor` in the same environment diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..f813546 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,136 @@ +# FAQ + +## Does the default policy really remove documentation comments? + +Yes. `safe` removes `doc-line` and `doc-block` along with ordinary comments; it +is *safe* in the sense that it never removes something another program reads, +not in the sense that it never removes something a human wrote. Documentation +comments are commentary, and a build artifact usually wants them gone. + +If your project wants them kept — this one does — say so once: + +```toml +[policy] +keep_kind = ["doc-line", "doc-block"] +``` + +[Why was this comment kept?](why-kept.md) has the full table of what each policy +does to each kind. + +## Does a bare `ocomment` check the whole repository or just here? + +Just here. A command that names no path checks the current directory, so running +it from a subdirectory checks that subdirectory. From the root of a repository +that is the whole repository: + +```sh +cd "$(git rev-parse --show-toplevel)" && ocomment +``` + +Glob settings do not move with you. `files.include`, `files.exclude`, and every +`[[overrides]].paths` pattern is relative to the project root — the directory +holding `.ocomment.toml`, or the repository above it — however deep you run +from. + +## Why did naming a path change what got scanned? + +Because naming it is a request rather than a default. A walk with no path skips +hidden files and files over 32 MiB; an explicit `ocomment .` or `ocomment src` +bypasses those two limits, on the grounds that you asked for that path by name. +The binary and symlink safety checks still apply either way. + +## Does it handle a partially staged file? + +Yes, with `--staged`. `ocomment check --staged` reads the Git index blobs — the +exact bytes the commit will carry — instead of the working tree, so a file whose +comment is staged but whose other edits are not is judged on the staged half +alone. `ocomment fix --staged` rewrites the index blobs and maps those edits back +to the working tree only where the mapping is unique; `--index-only` is the +escape hatch when it is not. + +The generated Lefthook hook deliberately does not use Lefthook's `stage_fixed`, +because that setting stages the whole working-tree file and destroys the partial +staging it was meant to protect. + +## What happens to a file that is not valid UTF-8? + +It is scanned anyway. The engine works on bytes, so a file with a Latin-1 name in +a string literal, or an encoding it has never heard of outside the regions it +edits, is read, reported on, and rewritten with those bytes untouched. Only the +spans it actually removes are changed. + +A file that fails to *lex* — an unterminated block comment, say — is reported as +invalid and left alone, unless `--force-invalid` tells the run to apply the edits +that are still provably safe. + +## Why is this comment still here after `fix`? + +Ask it: + +```sh +ocomment check --explain +``` + +`--explain` puts the rule that decided each comment, and the setting behind that +rule, on the line under it. Nine times in ten the answer is one of: the policy +does not remove that kind, a `keep_regex` or `keep_kind` in `.ocomment.toml` +protects it, an `[[overrides]]` entry matched the path, or the comment is a +directive that some other tool reads. See +[Why was this comment kept?](why-kept.md). + +## What do the exit codes mean? + +`0` clean, `1` findings, `2` failure. Specifically: `0` when nothing removable +was found and every requested change was applied, `1` when removable comments +were reported or a diff was printed, and `2` for an invalid source, +configuration, plugin, or I/O failure. + +`1` from `diff` or `fix --dry-run` means the patch is not empty, which is why a +CI gate can be `ocomment check` with nothing around it, and why a script that +tests `$? -ne 0` will misread a non-empty diff as an error. + +## Are CRLF line endings, BOMs, and the final newline preserved? + +Yes, all three. A removal replaces the comment's own bytes and nothing else, so +a CRLF file stays CRLF, a UTF-8 BOM stays where it was, and a file with no +trailing newline does not grow one. Every layout also preserves the *line count* +of the file: a comment that spanned three lines is replaced by something that +still spans three lines, so line numbers in stack traces and `git blame` keep +pointing at the same statements. [Policies and layouts](policies.md) shows what +each one leaves behind. + +## How fast is it, and how would I know? + +Fast enough that it is not the slow part of a hook. The release gate refuses to +publish a build that misses any of: a 20 ms median cold `--version`, 500 MiB/s +for the simple scanners, 200 MiB/s for JavaScript and Shell, a 25 MiB stripped +binary, and no more than a 5% regression against the checked-in baseline on a +fixed machine. Where `typos` is installed, it also requires a no-op repository +scan to be no slower than 1.5 times `typos` on the same tree. + +Those are gates rather than marketing numbers: measure on your own tree with +`ocomment -v`, which reports what was scanned and skipped. + +## How much do I have to trust a scanner plugin? + +Less than you would have to trust a normal plugin, by construction. A plugin is +a WebAssembly component that receives source bytes and returns comment spans and +kinds; it cannot edit a file, and the host rechecks the API version, the bounds, +the ordering, the overlap, the policy, and every edit generated from what it +returned. There is no WASI, no filesystem, no network, no clock, and no +randomness, and each invocation gets a fuel budget and explicit memory and +instance limits. + +Remote artifacts need a pinned SHA-256 and a Sigstore identity, recorded in +`.ocomment.lock`, and are fetched only by an explicit `plugin add` or +`plugin update`. Ordinary scans and LSP sessions never go to the network. See +[Plugins](plugins.md). + +## Does it work on Windows? + +Yes. Every release publishes an `x86_64-pc-windows-msvc` archive with +PowerShell completions in it, plus the Scoop and WinGet manifests generated from +that archive, and CI smoke tests the binary on a Windows runner alongside Linux +and macOS. CRLF line endings are preserved rather than normalised, which matters +more on Windows than anywhere else. The container image is Linux-only, as +container images are. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..082a63d --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,136 @@ +# Getting started + +## Install it + +```sh +cargo install ocomment --locked +``` + +That is one of several channels — a prebuilt archive, Homebrew, Scoop, WinGet, +a container image, a GitHub Action, and a pre-commit hook are all documented +under [Installation](installation.md). Everything below works the same way +whichever one you used. + +## Look before you leap + +`ocomment` with no command is `ocomment check`, and `check` with no path is the +current directory, so the shortest useful run is the tool's own name: + +```console +$ ocomment check src +src/main.rs:2:5: removable line comment: // TODO: drop this +Found 1 removable comment in 1 file (1 file scanned). Run `ocomment fix` to remove it. +``` + +Nothing has changed on disk. `check` only reports, and it reports the same set +of comments that `fix` would remove. + +## See the change as a patch + +```console +$ ocomment diff src +--- a/src/main.rs ++++ b/src/main.rs +@@ -1,4 +1,4 @@ + fn main() { +- // TODO: drop this ++ + println!("hello"); + } +Found 1 removable comment in 1 file (1 file scanned). Run `ocomment fix` to apply the patch. +``` + +The patch goes to standard output and the summary line goes to standard error, +so `ocomment diff src > fix.patch` writes a file `git apply` will take. The +blank line the removal leaves behind is the `lines` layout at work; `compact` +and `columns` leave something else, and [Policies and layouts](policies.md) +shows all three side by side. + +`ocomment fix --dry-run src` prints the same patch and applies nothing, which is +the form to reach for inside a script. + +## Make the change + +```console +$ ocomment fix src +fixed src/main.rs: removed 1 comment +Removed 1 comment in 1 file (1 file scanned). +``` + +Every edit of a run is prepared first and committed as one transaction, so an +interrupted `fix` leaves the tree as it found it rather than half-rewritten. + +`ocomment fix -i` asks about each comment instead, with three lines of context +either side: `y` removes it, `n` keeps it, `a` and `d` answer for the rest of +the file, `q` stops asking and applies what was accepted, and `x` abandons the +run without writing anything. + +## Exit codes + +| Code | Meaning | +| --- | --- | +| `0` | Nothing removable was found, and every requested change was applied. | +| `1` | Removable comments were reported, or a diff was printed. | +| `2` | An invalid source, configuration, plugin, or I/O failure. | + +That is why `ocomment check` works as a CI gate on its own, and why `1` from +`diff` is not an error: it means the patch is not empty. + +## Decide what your project keeps + +The default `safe` policy removes ordinary and documentation comments and keeps +source preambles and tool directives. Write the decision down instead of +passing flags every time: + +```sh +ocomment init config +``` + +`init config` writes a `.ocomment.toml` holding every default spelled out, so +the file starts as a complete description of what the tool already does and you +change the lines you disagree with. A project that has made a few decisions +ends up looking like this: + +```toml +version = 1 + +[policy] +mode = "legal" +layout = "lines" +keep_kind = ["doc-line", "doc-block"] +keep_regex = ['^//\s*NOTE\b'] + +[[overrides]] +paths = ["generated/**"] +policy = "all" +``` + +That file keeps licence headers, keeps documentation comments, keeps any +comment opening with `NOTE`, and takes everything out of `generated/`. +[Configuration](configuration.md) documents every key, and `ocomment config +explain` prints the resolved result with the source of each value. + +## Ask why a comment survived + +```sh +ocomment check --explain +``` + +`--explain` puts the rule that decided each comment, and the setting behind that +rule, on the line underneath it — for the comments it kept as much as the ones +it would remove. [Why was this comment kept?](why-kept.md) walks through a real +answer. + +## Put it in the loop + +```sh +ocomment init lefthook --fix +lefthook install +``` + +The generated hook runs `ocomment check --staged`, which judges the bytes the +commit will actually carry rather than the working tree — the distinction that +matters for a partially staged file. [CI and hooks](ci.md) covers the +pre-commit manifest, the composite GitHub Action, and SARIF upload to code +scanning; [Editors and LSP](editors.md) covers seeing the same diagnostics as +you type. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..be384cc --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,145 @@ +# Installation + +Every channel below installs the same binary for the same release. Pick one; +they do not need each other. Examples pin `0.1.0` — use the version you want, +and prefer a full pin over a moving tag wherever a workflow or a tap will +resolve it later. + +## From crates.io + +```sh +cargo install ocomment --locked +``` + +`--locked` builds against the dependency versions the release was tested with. +This is the only channel that compiles on your machine, so it needs a Rust +toolchain of 1.88 or newer and takes a few minutes. + +## Prebuilt binary with cargo-binstall + +```sh +cargo binstall ocomment +``` + +The crate carries the archive URL, the archive format, and the path of the +binary inside it as `package.metadata.binstall`, so `cargo-binstall` downloads +the release archive for your target instead of compiling anything. + +## Release archives + +Every release publishes one archive per target: + +| Platform | Asset | +| --- | --- | +| Linux x86-64 (glibc) | `ocomment-x86_64-unknown-linux-gnu.tar.gz` | +| Linux ARM64 (glibc) | `ocomment-aarch64-unknown-linux-gnu.tar.gz` | +| Linux x86-64 (musl, static) | `ocomment-x86_64-unknown-linux-musl.tar.gz` | +| Linux ARM64 (musl, static) | `ocomment-aarch64-unknown-linux-musl.tar.gz` | +| macOS Intel | `ocomment-x86_64-apple-darwin.tar.gz` | +| macOS Apple Silicon | `ocomment-aarch64-apple-darwin.tar.gz` | +| Windows x64 | `ocomment-x86_64-pc-windows-msvc.zip` | + +Each archive unpacks into an `ocomment-/` directory holding the binary, +both licences, the README, the `ocomment.1` manual page, and completion scripts +for Bash, Zsh, fish, PowerShell, and Elvish. + +```sh +gh release download v0.1.0 --repo P4suta/OComment \ + --pattern 'ocomment-x86_64-unknown-linux-gnu.tar.gz*' +tar -xzf ocomment-x86_64-unknown-linux-gnu.tar.gz +install -m 0755 ocomment-x86_64-unknown-linux-gnu/ocomment ~/.local/bin/ocomment +``` + +Do not skip [Verifying downloads](verify.md): every archive ships a SHA-256, a +Sigstore signature bundle, and a GitHub build-provenance attestation, and +checking them is three commands. + +## Homebrew, Scoop, and WinGet + +Every release generates and signs a Homebrew formula (`ocomment.rb`), a Scoop +manifest (`ocomment-scoop.json`), and a WinGet manifest (`ocomment.winget.yaml`) +from the SHA-256 values of the archives that release actually built. They are +attached to the release as assets and can be installed directly: + +```sh +brew install --formula ./ocomment.rb +``` + +```powershell +scoop install .\ocomment-scoop.json +winget install --manifest .\ocomment.winget.yaml +``` + +There is no published tap, bucket, or WinGet listing yet. Once those exist, the +same generated files are what gets submitted to them, and the commands become +the ordinary `brew install ocomment`, `scoop install ocomment`, and +`winget install OComment.OComment`. + +## Container image + +```sh +docker run --rm -v "$PWD:/src" ghcr.io/p4suta/ocomment:0.1.0 check +``` + +The image is `scratch` plus one statically linked musl binary, built from the +exact archives of the same release rather than from a second compilation. +[Docker](docker.md) covers writing files back, exit codes, and running as your +own user. + +## GitHub Actions + +```yaml + - uses: P4suta/OComment@v0.1.0 + with: + paths: src tests +``` + +The composite action downloads the release archive for the runner, verifies its +SHA-256 and its build-provenance attestation, and annotates the pull request. +[CI and hooks](ci.md) documents every input and output, including SARIF upload +to code scanning. + +## pre-commit + +```yaml +repos: + - repo: https://github.com/P4suta/OComment + rev: v0.1.0 + hooks: + - id: ocomment-check +``` + +The hooks are `language: system`, so install the CLI through one of the channels +above first. [CI and hooks](ci.md) explains why, and what `args: ["--staged"]` +changes for a partially staged file. + +## Editor extension + +The official VS Code extension is `P4suta.ocomment`, on the Visual Studio +Marketplace and on Open VSX. It is a client only: it launches the `ocomment` +binary, so install both. Every other LSP client can launch `ocomment lsp` +directly — see [Editors and LSP](editors.md). + +## From source + +```sh +git clone https://github.com/P4suta/OComment +cd OComment +cargo build --manifest-path rust/Cargo.toml --release --locked -p ocomment +``` + +The binary lands at `rust/target/release/ocomment`. Building the OCaml reference +implementation and running the differential suite additionally needs OCaml 5.5, +opam, Dune, and Python 3; see +[CONTRIBUTING.md](https://github.com/P4suta/OComment/blob/main/CONTRIBUTING.md). + +## Check what you got + +```sh +ocomment --version +ocomment doctor +``` + +`doctor` reports the resolved configuration, the Git integration, the plugin +lock, and the external tools it can find, which is the fastest way to see that +an install is complete and that a hook or an editor will be able to launch it. diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 0000000..e925b62 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,63 @@ +# OComment + +OComment is a fast, byte-preserving comment checker and remover. It reads source +bytes without requiring them to be UTF-8, reports every comment it finds, and +removes the ones a policy allows it to remove — through a rollback-backed +transaction, so a run either applies every edit or none of them. + +The production tool is the Rust `ocomment` binary and the public `ocomment-core` +library. `ocomment-ref` is an independent OCaml implementation, and the two are +compared on the scanner, the classification, the diagnostics, the edits, the +transformed bytes, and the source maps. Nothing is shared between them; matching +normalized output is the cross-check. + +```console +$ ocomment check src +src/main.rs:2:5: removable line comment: // TODO: drop this +Found 1 removable comment in 1 file (1 file scanned). Run `ocomment fix` to remove it. +``` + +## What it is for + +A comment remover is usually wanted for one of three reasons, and OComment is +built for all three: + +- **Shipping less than you wrote.** Stripping comments out of a build artifact, + a container image, or a vendored copy, without changing what the code does. +- **Holding a line in review.** A team that has agreed which comments are worth + keeping can encode that agreement in `.ocomment.toml` and let the + [pre-commit hook or CI](ci.md) enforce it. This repository does exactly that + to itself. +- **Reading what a file really says.** `ocomment scan` gives the kind, the + disposition, and the byte span of every comment, for a tool to consume. + +## What it will not do + +It will not remove a comment that is not commentary. A shebang, an encoding +preamble, a build tag, a lint control, an optimiser hint, and a MySQL versioned +comment all change what some other program does with the file, and the default +policy keeps every one of them. [Why was this comment kept?](why-kept.md) is the +page about that, and `--explain` answers it for any single comment. + +It will not corrupt a file it does not understand. The engine never requires the +complete source to be UTF-8, so BOMs, CRLF line endings, trailing newlines, and +non-UTF-8 bytes outside the edited spans come back exactly as they went in. + +## Where to go next + +- [Getting started](getting-started.md) is the five-minute version. +- [Installation](installation.md) lists every channel the tool ships through. +- [Commands](commands.md) is the complete CLI reference, generated from the + binary. +- [Configuration](configuration.md) is `.ocomment.toml` in full. +- [Policies and layouts](policies.md) shows what each setting does to one + sample file. +- [Languages and dialects](languages.md) is what OComment can read. +- [Editors and LSP](editors.md), [CI and hooks](ci.md), and [Docker](docker.md) + cover the integrations. +- [Library](library.md) and [Plugins](plugins.md) are for building on it. + +OComment is available under either the +[MIT license](https://github.com/P4suta/OComment/blob/main/LICENSE-MIT) or the +[Apache License 2.0](https://github.com/P4suta/OComment/blob/main/LICENSE-APACHE), +at your option. diff --git a/docs/languages.md b/docs/languages.md new file mode 100644 index 0000000..327cc04 --- /dev/null +++ b/docs/languages.md @@ -0,0 +1,56 @@ + + +# Languages and dialects + +`spec/languages.toml` is the canonical table. The `files:` pattern of the +published pre-commit hooks is generated from it, `tools/check_hooks.py` fails +when the two drift apart, and the table below is generated from the same file. +`ocomment languages` prints a shorter summary of it; this is the complete one. + +A dialect changes the lexical rules rather than the file type: `--dialect +mysql` is still SQL, and only that dialect treats `/*!40101 ... */` as +something the server executes rather than as a comment. + +A language is chosen from the file extension, and `--language` overrides that +for a run — which is what `ocomment strip` needs, because standard input has no +name. `--dialect` picks the dialect for the same run, and `[languages.] +dialect = "..."` in `.ocomment.toml` picks one for everybody working in the +repository. An incompatible pair is an error rather than a silent fallback: + +```console +$ ocomment strip --language rust --dialect mysql +ocomment: unsupported dialect `mysql` for rust; supported: standard +``` + +OComment has 15 built-in languages covering +37 file extensions and 16 named dialects. + +| Language | Extensions | Dialects | +| --- | --- | --- | +| `rust` | `.rs` | `standard` | +| `ocaml` | `.ml`, `.mli` | `standard` | +| `c` | `.c`, `.h`, `.m` | `standard`, `gnu-c`, `objective-c` | +| `cpp` | `.cc`, `.cpp`, `.cxx`, `.hpp`, `.mm`, `.cu` | `standard`, `gnu-cpp`, `objective-cpp`, `cuda` | +| `go` | `.go` | `standard` | +| `java` | `.java` | `standard` | +| `javascript` | `.js`, `.mjs`, `.cjs`, `.jsx` | `standard`, `jsx` | +| `typescript` | `.ts`, `.mts`, `.cts`, `.tsx` | `standard`, `tsx` | +| `python` | `.py`, `.pyw`, `.pyi` | `standard` | +| `shell` | `.sh`, `.bash`, `.zsh` | `posix-sh`, `bash53`, `zsh` | +| `html` | `.html`, `.htm`, `.xhtml` | `standard` | +| `css` | `.css` | `standard` | +| `jsonc` | `.jsonc`, `.json5` | `standard` | +| `sql` | `.sql` | `standard`, `postgresql`, `mysql`, `sqlite`, `t-sql`, `oracle` | +| `kotlin` | `.kt`, `.kts` | `standard` | + +## Anything else + +HTML is scanned recursively: the contents of a `", - "note": "DOM-observable HTML comment and recursive script" + "note": "HTML Standard 13.1.6 comments and 13.2.5 script data: a `` comment is exposed to scripts through the DOM, and a `" + } + }, + { + "id": "html-builtin-all", + "language": "html", + "operation": "transform", + "options": { + "policy": "all", + "layout": "lines" + }, + "source_utf8": "", + "note": "HTML Standard 13.1.6 comments and 13.2.5 script data: a `` comment is exposed to scripts through the DOM, and a `" + } + }, + { + "id": "css-builtin-safe", + "language": "css", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "a{content:\"/* raw */\";/* block */}\n", + "note": "CSS Syntax Level 3, 4.3.1 consume a token: a `/* */` inside a string is opaque, and the one beside it is a comment.", + "expect": { + "valid": true, + "comments": [ + { + "start": 22, + "end": 33, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "a{content:\"/* raw */\"; }\n" + } }, { + "id": "css-builtin-all", "language": "css", + "operation": "transform", + "options": { + "policy": "all", + "layout": "lines" + }, "source_utf8": "a{content:\"/* raw */\";/* block */}\n", - "note": "string opacity" + "note": "CSS Syntax Level 3, 4.3.1 consume a token: a `/* */` inside a string is opaque, and the one beside it is a comment.", + "expect": { + "valid": true, + "comments": [ + { + "start": 22, + "end": 33, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "a{content:\"/* raw */\"; }\n" + } + }, + { + "id": "jsonc-builtin-safe", + "language": "jsonc", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "{\"x\":\"// raw\" // line\n}\n", + "note": "JSON with comments: a JSON string hides `//`, and the comment after the value is found.", + "expect": { + "valid": true, + "comments": [ + { + "start": 14, + "end": 21, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "{\"x\":\"// raw\" \n}\n" + } }, { + "id": "jsonc-builtin-all", "language": "jsonc", + "operation": "transform", + "options": { + "policy": "all", + "layout": "lines" + }, "source_utf8": "{\"x\":\"// raw\" // line\n}\n", - "note": "JSON string opacity" + "note": "JSON with comments: a JSON string hides `//`, and the comment after the value is found.", + "expect": { + "valid": true, + "comments": [ + { + "start": 14, + "end": 21, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "{\"x\":\"// raw\" \n}\n" + } }, { + "id": "sql-builtin-safe", "language": "sql", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, "source_utf8": "select '-- raw'; -- line\n/* block */\n", - "note": "SQL string opacity and both comment forms" + "note": "SQL standard string literals and both comment forms: `--` inside a literal is opaque, and the `--` and `/* */` beside it are comments.", + "expect": { + "valid": true, + "comments": [ + { + "start": 17, + "end": 24, + "kind": "line", + "action": "remove" + }, + { + "start": 25, + "end": 36, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "select '-- raw'; \n\n" + } + }, + { + "id": "sql-builtin-all", + "language": "sql", + "operation": "transform", + "options": { + "policy": "all", + "layout": "lines" + }, + "source_utf8": "select '-- raw'; -- line\n/* block */\n", + "note": "SQL standard string literals and both comment forms: `--` inside a literal is opaque, and the `--` and `/* */` beside it are comments.", + "expect": { + "valid": true, + "comments": [ + { + "start": 17, + "end": 24, + "kind": "line", + "action": "remove" + }, + { + "start": 25, + "end": 36, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "select '-- raw'; \n\n" + } + }, + { + "id": "kotlin-builtin-safe", + "language": "kotlin", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "val s = \"// raw\" // line\n/* outer /* nested */ end */", + "note": "Kotlin specification, string literals and comments: the `//` inside the literal is opaque, the line comment is found, and block comments nest.", + "expect": { + "valid": true, + "comments": [ + { + "start": 17, + "end": 24, + "kind": "line", + "action": "remove" + }, + { + "start": 25, + "end": 53, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "val s = \"// raw\" \n" + } }, { + "id": "kotlin-builtin-all", "language": "kotlin", + "operation": "transform", + "options": { + "policy": "all", + "layout": "lines" + }, "source_utf8": "val s = \"// raw\" // line\n/* outer /* nested */ end */", - "note": "string opacity and nested comments" + "note": "Kotlin specification, string literals and comments: the `//` inside the literal is opaque, the line comment is found, and block comments nest.", + "expect": { + "valid": true, + "comments": [ + { + "start": 17, + "end": 24, + "kind": "line", + "action": "remove" + }, + { + "start": 25, + "end": 53, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "val s = \"// raw\" \n" + } } ] } diff --git a/spec/fixtures/v1/hazards.json b/spec/fixtures/v1/hazards.json new file mode 100644 index 0000000..de47dfe --- /dev/null +++ b/spec/fixtures/v1/hazards.json @@ -0,0 +1,2330 @@ +{ + "version": 1, + "cases": [ + { + "id": "rust-nested-raw", + "language": "rust", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "r#\"// opaque\"# /* outer /* inner */ end */\\n// rustfmt::skip\\n", + "note": "Rust Reference, Tokens: raw string literals take no escapes, and block comments nest. The raw string hides `//`, `/* outer /* inner */ end */` is one comment, and `// rustfmt::skip` is a directive `safe` keeps.", + "expect": { + "valid": true, + "comments": [ + { + "start": 15, + "end": 42, + "kind": "block", + "action": "remove" + }, + { + "start": 44, + "end": 62, + "kind": "directive", + "action": "keep" + } + ], + "diagnostics": [], + "output_utf8": "r#\"// opaque\"# \\n// rustfmt::skip\\n" + } + }, + { + "id": "rust-raw-c-string", + "language": "rust", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "cr#\"inner \" // opaque\"#; // remove\n", + "note": "Rust Reference, Tokens (raw C string literals, Rust 1.77): `cr#\"...\"#` ends only at `\"#`, so the unescaped `\"` and the `//` inside it are opaque and only the trailing line comment is found.", + "expect": { + "valid": true, + "comments": [ + { + "start": 25, + "end": 34, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "cr#\"inner \" // opaque\"#; \n" + } + }, + { + "id": "rust-multiline-string", + "language": "rust", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "const A: &str = \"a\n// opaque\nb\"; // remove\n", + "note": "Rust Reference, Tokens: a string literal may contain newlines, so `//` on an inner line is opaque and only the trailing line comment is found.", + "expect": { + "valid": true, + "comments": [ + { + "start": 33, + "end": 42, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "const A: &str = \"a\n// opaque\nb\"; \n" + } + }, + { + "id": "ocaml-nested-quoted", + "language": "ocaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "{tag| (* opaque *) |tag} (* outer \"*)\" (* inner *) *)", + "note": "OCaml manual, Lexical conventions: quoted string literals `{id|...|id}` take no escapes, and comments nest. The quoted string hides `(*`, and the nested comment closes once.", + "expect": { + "valid": true, + "comments": [ + { + "start": 25, + "end": 53, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "{tag| (* opaque *) |tag} " + } + }, + { + "id": "ocaml-comment-quoted", + "language": "ocaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "(* outer {tag| *) opaque |tag} end *)", + "note": "OCaml manual, Comments: a comment may contain a string literal, and the literal's contents are not scanned for `*)`, so the `*)` inside `{tag|...|tag}` does not close the comment.", + "expect": { + "valid": true, + "comments": [ + { + "start": 0, + "end": 37, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "" + } + }, + { + "id": "ocaml-long-quoted-id", + "language": "ocaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|(* opaque *)|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} (* remove *)", + "note": "OCaml manual, Lexical conventions: the identifier of a quoted string literal has no length limit, so an 80-character tag must still be matched exactly at the close.", + "expect": { + "valid": true, + "comments": [ + { + "start": 177, + "end": 189, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|(* opaque *)|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} " + } + }, + { + "id": "invalid-ocaml-quoted", + "language": "ocaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "{tag| unterminated (* opaque *)", + "note": "OCaml manual, Lexical conventions: an unterminated quoted string literal is a lexical error, reported as an error diagnostic that makes the report invalid.", + "expect": { + "valid": false, + "comments": [], + "diagnostics": [ + { + "code": "unterminated-string", + "start": 0, + "end": 31 + } + ], + "output_utf8": "{tag| unterminated (* opaque *)" + } + }, + { + "id": "c-line-splice", + "language": "c", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "int x; /\\\n/ comment\\\ncontinued\nint y;", + "note": "ISO C17 5.1.1.2 translation phases: backslash-newline splicing (phase 2) happens before comments are recognised (phase 3), so `/\\/` opens a line comment and a spliced line comment runs on to the next line.", + "expect": { + "valid": true, + "comments": [ + { + "start": 7, + "end": 30, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "int x; \n\n\nint y;" + } + }, + { + "id": "cpp-raw", + "language": "cpp", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "R\"tag(/* opaque */ // opaque)tag\" // remove", + "note": "ISO C++ [lex.string]: a raw string literal `R\"tag(...)tag\"` ends only at the matching delimiter, so both comment forms inside it are opaque.", + "expect": { + "valid": true, + "comments": [ + { + "start": 34, + "end": 43, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "R\"tag(/* opaque */ // opaque)tag\" " + } + }, + { + "id": "go-directives", + "language": "go", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "//go:build linux\n// +build linux\n//line generated.go:1\n// remove\n", + "note": "Go compiler directives: `//go:build`, the legacy `// +build` constraint, and `//line` are directives `safe` keeps; the plain line comment is removed.", + "expect": { + "valid": true, + "comments": [ + { + "start": 0, + "end": 16, + "kind": "directive", + "action": "keep" + }, + { + "start": 17, + "end": 32, + "kind": "directive", + "action": "keep" + }, + { + "start": 33, + "end": 54, + "kind": "directive", + "action": "keep" + }, + { + "start": 55, + "end": 64, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "//go:build linux\n// +build linux\n//line generated.go:1\n\n" + } + }, + { + "id": "java-unicode", + "language": "java", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "int x; \\u002f\\u002f comment\\u000aint y;", + "note": "JLS 3.3 Unicode escapes: `\\uXXXX` is translated before lexing, so `\\u002f\\u002f` opens a line comment and `\\u000a` is the line terminator that ends it.", + "expect": { + "valid": true, + "comments": [ + { + "start": 7, + "end": 27, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "int x; \\u000aint y;" + } + }, + { + "id": "java-unicode-surrogates", + "language": "java", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "String s = \"\\uD83D\\uDE00 // opaque\"; // remove", + "note": "JLS 3.3: a surrogate pair written as two Unicode escapes is one code point; it is inside a string literal, so the `//` after it is opaque and only the trailing comment is found.", + "expect": { + "valid": true, + "comments": [ + { + "start": 37, + "end": 46, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "String s = \"\\uD83D\\uDE00 // opaque\"; " + } + }, + { + "id": "invalid-java-unicode", + "language": "java", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "int x = 1; \\u00G0 // known", + "note": "JLS 3.3: an eligible backslash not followed by four hexadecimal digits is a compile-time error, reported as an error diagnostic while the trailing comment is still located.", + "expect": { + "valid": false, + "comments": [ + { + "start": 18, + "end": 26, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [ + { + "code": "invalid-unicode-escape", + "start": 11, + "end": 17 + } + ], + "output_utf8": "int x = 1; \\u00G0 // known" + } + }, + { + "id": "forced-invalid-java-unicode", + "language": "java", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines", + "force_invalid": true + }, + "source_utf8": "int x = 1; \\u00G0 // known", + "note": "The `invalid-java-unicode` source with `force_invalid`: the error diagnostic stands, and edits are produced anyway.", + "expect": { + "valid": false, + "comments": [ + { + "start": 18, + "end": 26, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [ + { + "code": "invalid-unicode-escape", + "start": 11, + "end": 17 + } + ], + "output_utf8": "int x = 1; \\u00G0 " + } + }, + { + "id": "java-text-block-escape", + "language": "java", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "String s = \"\"\"\n\\\"\"\" // opaque\nend\n\"\"\"; // remove\n", + "note": "JLS 3.10.6 text blocks: an escaped `\\\"\"\"` does not close the block, so the `//` inside it is opaque.", + "expect": { + "valid": true, + "comments": [ + { + "start": 39, + "end": 48, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "String s = \"\"\"\n\\\"\"\" // opaque\nend\n\"\"\"; \n" + } + }, + { + "id": "javascript-goals", + "language": "javascript", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "#!/usr/bin/env node\nconst r = /\\/\\/* opaque/;\nconst t = `literal // opaque ${1 /* remove */}`;\n// remove\n", + "note": "ECMA-262 lexical goal symbols: a `/` after `=` starts a regular expression, a template literal's text is opaque, and its substitution returns to the code goal. The hashbang (ECMA-262 Hashbang Grammar) is protected.", + "expect": { + "valid": true, + "comments": [ + { + "start": 0, + "end": 19, + "kind": "shebang", + "action": "keep" + }, + { + "start": 79, + "end": 91, + "kind": "block", + "action": "remove" + }, + { + "start": 95, + "end": 104, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "#!/usr/bin/env node\nconst r = /\\/\\/* opaque/;\nconst t = `literal // opaque ${1 }`;\n\n" + } + }, + { + "id": "javascript-control-regex", + "language": "javascript", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "if (ready) /https?:\\/\\/example\\.test/.test(value); // remove", + "note": "ECMA-262 InputElementRegExp: after the `)` closing an `if` condition a `/` starts a regular expression, so the escaped `\\/\\/` in it is opaque.", + "expect": { + "valid": true, + "comments": [ + { + "start": 51, + "end": 60, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "if (ready) /https?:\\/\\/example\\.test/.test(value); " + } + }, + { + "id": "javascript-brace-goals", + "language": "javascript", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "const ratio = {} / 2; // remove\nif (ready) {} /[/*]/.test(value); // remove\n", + "note": "ECMA-262 lexical goal symbols: `{}` after `=` is an object literal and the following `/` is division, while `{}` after `if (...)` is a block and the following `/` starts a regular expression.", + "expect": { + "valid": true, + "comments": [ + { + "start": 22, + "end": 31, + "kind": "line", + "action": "remove" + }, + { + "start": 66, + "end": 75, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "const ratio = {} / 2; \nif (ready) {} /[/*]/.test(value); \n" + } + }, + { + "id": "javascript-html-like-comments", + "language": "javascript", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "const x = 1; remove\nconst text = '` is one at the start of a line, but `", + "note": "Layout `columns` after a removed HTML comment that spans lines: column positions are recomputed from the current line, not from the original offsets.", + "expect": { + "valid": true, + "comments": [ + { + "start": 2, + "end": 20, + "kind": "html-comment", + "action": "remove" + }, + { + "start": 36, + "end": 41, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "ab" + } + }, + { + "id": "non-utf8-bytes", + "language": "c", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_base64": "/y8qIHJlbW92ZSAqL4ANCg==", + "note": "Bytes that are not UTF-8 around a comment are copied through untouched, and the CRLF ending survives.", + "expect": { + "valid": true, + "comments": [ + { + "start": 1, + "end": 13, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_base64": "/yCADQo=" + } + }, + { + "id": "compact-layout", + "language": "c", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "left/* remove */right\n", + "note": "Layout `compact` over a comment between two tokens on one line: nothing about the line changes but the comment, so `compact` leaves exactly what `lines` leaves, the separating space included.", + "expect": { + "valid": true, + "comments": [ + { + "start": 4, + "end": 16, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "left right\n" + } + }, + { + "id": "compact-whole-line-run", + "language": "rust", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "fn main() {}\n// one\n// two\nlet x = 1;\n", + "note": "Layout `compact` takes a line that held nothing but a removed comment away entirely, its terminator included, so a run of whole-line comments disappears instead of leaving a run of blank lines. Layout `lines` keeps all four lines.", + "expect": { + "valid": true, + "comments": [ + { + "start": 13, + "end": 19, + "kind": "line", + "action": "remove" + }, + { + "start": 20, + "end": 26, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "fn main() {}\nlet x = 1;\n" + } + }, + { + "id": "compact-indented-line", + "language": "rust", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "fn main() {\n // note\n let x = 1;\n}\n", + "note": "Layout `compact`: only whitespace stood before the comment on its line, so the indentation goes with the line rather than staying behind as trailing whitespace.", + "expect": { + "valid": true, + "comments": [ + { + "start": 16, + "end": 23, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "fn main() {\n let x = 1;\n}\n" + } + }, + { + "id": "compact-crlf-line", + "language": "rust", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "let x = 1;\r\n// note\r\nlet y = 2;\r\n", + "note": "Layout `compact` over CRLF endings: the terminator removed with the line is that line's own CRLF, and the lines that survive keep theirs byte for byte.", + "expect": { + "valid": true, + "comments": [ + { + "start": 12, + "end": 19, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "let x = 1;\r\nlet y = 2;\r\n" + } + }, + { + "id": "compact-trailing-whitespace", + "language": "rust", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "let x = 1; \t // note\nlet y = 2;\t/* two */\t\nlet z = 3;\n", + "note": "Layout `compact` trims the whitespace a removal would otherwise leave at the end of a line, on both sides of the comment. Code survives on each line, so each keeps its terminator.", + "expect": { + "valid": true, + "comments": [ + { + "start": 13, + "end": 20, + "kind": "line", + "action": "remove" + }, + { + "start": 32, + "end": 41, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "let x = 1;\nlet y = 2;\nlet z = 3;\n" + } + }, + { + "id": "compact-no-final-newline", + "language": "rust", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "let x = 1; // note", + "note": "Layout `compact` over a file that ends without a final newline: the trailing comment and the space before it go, and no terminator is invented for the line that survives.", + "expect": { + "valid": true, + "comments": [ + { + "start": 11, + "end": 18, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "let x = 1;" + } + }, + { + "id": "compact-last-line-only-comment", + "language": "rust", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "let x = 1;\n// note", + "note": "Layout `compact`: the last line held nothing but the comment and had no terminator of its own, so the line goes and the line before it keeps the terminator it always had.", + "expect": { + "valid": true, + "comments": [ + { + "start": 11, + "end": 18, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "let x = 1;\n" + } + }, + { + "id": "compact-block-shares-lines-with-code", + "language": "c", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "int a = 1; /* one\ntwo\nthree */ int b = 2;\n", + "note": "ISO C17 6.4.9p1: `/*` ... `*/` is one comment however many lines it spans. Layout `compact` keeps the code before it on a line of its own, ends that line with the terminator that ended it in the source, and drops the lines that held only comment.", + "expect": { + "valid": true, + "comments": [ + { + "start": 11, + "end": 30, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "int a = 1;\n int b = 2;\n" + } + }, + { + "id": "compact-block-alone-on-its-lines", + "language": "c", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "int a = 1;\n/* one\ntwo */\nint b = 2;\n", + "note": "ISO C17 6.4.9p1. Layout `compact`: every line the block comment covered held nothing else, so all of them go, terminators included.", + "expect": { + "valid": true, + "comments": [ + { + "start": 11, + "end": 24, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "int a = 1;\nint b = 2;\n" + } + }, + { + "id": "compact-block-at-end-without-newline", + "language": "c", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "int x = 1; /* one\ntwo */", + "note": "ISO C17 6.4.9p1. Layout `compact` over a block comment that runs to the end of a file with no final newline: the line the code is on kept its terminator inside the comment, so that terminator comes back, and the last line held only comment and goes.", + "expect": { + "valid": true, + "comments": [ + { + "start": 11, + "end": 24, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "int x = 1;\n" + } + }, + { + "id": "compact-two-comments-on-one-line", + "language": "c", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "a/* one */ /* two */\n", + "note": "Layout `compact` judges being alone on a line from the original bytes: neither comment was, so the line keeps its terminator, and the trim closes the gap the first removal left before the second.", + "expect": { + "valid": true, + "comments": [ + { + "start": 1, + "end": 10, + "kind": "block", + "action": "remove" + }, + { + "start": 11, + "end": 20, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "a\n" + } + }, + { + "id": "compact-html-comment", + "language": "html", + "operation": "transform", + "options": { + "policy": "all", + "layout": "compact" + }, + "source_utf8": "

a

\n\n

b

\n", + "note": "HTML 13.2.5.4x: `` is one comment token. An HTML comment closes up completely under every layout, the newlines it spanned included; `compact` adds the whole-line rule and the trim, and never takes the terminator of a line where code survives.", + "expect": { + "valid": true, + "comments": [ + { + "start": 9, + "end": 22, + "kind": "html-comment", + "action": "remove" + }, + { + "start": 32, + "end": 48, + "kind": "html-comment", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "

a

\n

b

\n" + } + }, + { + "id": "compact-javascript-line-separator", + "language": "javascript", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_base64": "bGV0IGEgPSAxO+KAqC8vIG5vdGXigKhsZXQgYiA9IDI7Cg==", + "note": "ECMA-262 12.3: U+2028 LINE SEPARATOR is a LineTerminator, so it ends a SingleLineComment. Layout `compact` removes it with the line it terminated and leaves the other one alone.", + "expect": { + "valid": true, + "comments": [ + { + "start": 13, + "end": 20, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_base64": "bGV0IGEgPSAxO+KAqGxldCBiID0gMjsK" + } + }, + { + "id": "compact-kept-comment-holds-its-line", + "language": "rust", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "// rustfmt::skip\n// note\nfn main() {}\n", + "note": "Layout `compact` over a kept directive above a removed comment: a comment that survives is content on its line, so only the line of the removed one goes.", + "expect": { + "valid": true, + "comments": [ + { + "start": 0, + "end": 16, + "kind": "directive", + "action": "keep" + }, + { + "start": 17, + "end": 24, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "// rustfmt::skip\nfn main() {}\n" + } + }, + { + "id": "invalid-cpp-raw", + "language": "cpp", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "R\"tag(unterminated /* opaque */", + "note": "ISO C++ [lex.string]: an unterminated raw string literal is a lexical error, reported as an error diagnostic that makes the report invalid.", + "expect": { + "valid": false, + "comments": [], + "diagnostics": [ + { + "code": "unterminated-string", + "start": 0, + "end": 31 + } + ], + "output_utf8": "R\"tag(unterminated /* opaque */" + } + }, + { + "id": "invalid-shell-quote", + "language": "shell", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "echo 'unterminated", + "note": "POSIX 2.2.2 single quotes: an unterminated single quote is a lexical error, reported as an error diagnostic that makes the report invalid.", + "expect": { + "valid": false, + "comments": [], + "diagnostics": [ + { + "code": "unterminated-string", + "start": 5, + "end": 18 + } + ], + "output_utf8": "echo 'unterminated" + } + }, + { + "id": "invalid-shell-heredoc", + "language": "shell", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "cat < bytes: + """The sample as a file, with `comment` where the marker goes.""" + return self.template.replace(SLOT, comment, 1).encode() # INVARIANT: One sample for each name in `spec/directives.toml`, and one name @@ -73,123 +95,156 @@ class Sample: # INVARIANT: the shared spec fails here until a sample proves the scanner knows # INVARIANT: it. A name is a category (`shebang`, `lint-and-formatter`) as often # INVARIANT: as it is a literal prefix, which is why the sample is written out -# INVARIANT: rather than derived from the name. +# INVARIANT: rather than derived from the name -- and why the near-miss beside +# INVARIANT: it is written from the marker rather than from the name too. SAMPLES: dict[str, Sample] = { "shebang": Sample( - "shell", None, b"#!/bin/sh\n# control\n", KEPT_AS_PREAMBLE, "# {}" + "shell", + None, + f"{SLOT}\n# control\n", + "#!/bin/sh", + # NOTE: Every `#!` line at the first byte is a shebang, whatever + # NOTE: interpreter follows, so running letters on past `/bin/sh` would + # NOTE: still be one. What the rule also promises is that the `!` + # NOTE: touches the `#`, and that is what the near-miss takes away. + "# !/bin/shish note", + KEPT_AS_PREAMBLE, ), "encoding": Sample( "python", None, - b"# -*- coding: utf-8 -*-\n# control\n", + f"{SLOT}\n# control\n", + "# -*- coding: utf-8 -*-", + "# -*- codingish: utf-8 -*-", KEPT_AS_PREAMBLE, - "# {}", ), "go:": Sample( "go", None, - b"//go:build linux\n// control\n", - KEPT_AS_DIRECTIVE, - "// {}", + f"{SLOT}\n// control\n", + "//go:build linux", # NOTE: `//go:` is a namespace: every Go directive is spelled # NOTE: `//go:`, so `//go:ish` is exactly the shape of one and # NOTE: protecting it is right. What the marker still promises is that # NOTE: it opens the comment, so the near-miss mentions it instead. - negative="a note about go:build linux", + "// a note about go:build linux", + KEPT_AS_DIRECTIVE, ), "+build": Sample( "go", None, - b"// +build linux\n// control\n", + f"{SLOT}\n// control\n", + "// +build linux", + "// a note about +build linux", KEPT_AS_DIRECTIVE, - "// {}", - negative="a note about +build linux", ), "triple-slash-reference": Sample( "typescript", None, - b'/// \n// control\n', + f"{SLOT}\n// control\n", + '/// ', + # NOTE: The marker is a shape rather than a word: a `///` comment + # NOTE: opening with `<` is a reference whatever element follows, so + # NOTE: the boundary left to get wrong is the opener. Two slashes are + # NOTE: an ordinary comment that happens to quote the directive. + '// ', KEPT_AS_DIRECTIVE, - "// {}", ), "sourceMappingURL": Sample( "javascript", None, - b"//# sourceMappingURL=bundle.js.map\n// control\n", + f"{SLOT}\n// control\n", + "//# sourceMappingURL=bundle.js.map", + f"//# sourceMappingURL{NEGATIVE_SUFFIX}", KEPT_AS_DIRECTIVE, - "// {}", ), "sourceURL": Sample( "javascript", None, - b"//# sourceURL=bundle.js\n// control\n", + f"{SLOT}\n// control\n", + "//# sourceURL=bundle.js", + f"//# sourceURL{NEGATIVE_SUFFIX}", KEPT_AS_DIRECTIVE, - "// {}", ), "#__PURE__": Sample( "javascript", None, - b"const value = /*#__PURE__*/ factory();\n// control\n", - KEPT_AS_DIRECTIVE, - "// {}", + f"const value = {SLOT} factory();\n// control\n", + "/*#__PURE__*/", # NOTE: The annotation ends in its own delimiter, so there is no word # NOTE: boundary after it to get wrong; `#__PURE__ish` is still the # NOTE: bundler's marker with rubbish appended. - negative="a note about #__PURE__ elsewhere", + "/* a note about #__PURE__ elsewhere */", + KEPT_AS_DIRECTIVE, ), "@__PURE__": Sample( "javascript", None, - b"const value = /*@__PURE__*/ factory();\n// control\n", + f"const value = {SLOT} factory();\n// control\n", + "/*@__PURE__*/", + "/* a note about @__PURE__ elsewhere */", KEPT_AS_DIRECTIVE, - "// {}", - negative="a note about @__PURE__ elsewhere", ), "lint-and-formatter": Sample( "javascript", None, - b"// eslint-disable-next-line no-eval\n// control\n", + f"{SLOT}\n// control\n", + "// eslint-disable-next-line no-eval", + # NOTE: `eslint` is a namespace as much as `go:` is -- every rule of + # NOTE: it is spelled `eslint-` -- so the near-miss is again + # NOTE: the comment that talks about the directive instead of being it. + "// a note about eslint-disable-next-line", KEPT_AS_DIRECTIVE, - "// {}", ), "type-checker": Sample( "python", None, - b"value = 1 # type: ignore\n# control\n", + f"value = 1 {SLOT}\n# control\n", + "# type: ignore", + # NOTE: The marker is matched as a bare prefix, so what is left to get + # NOTE: wrong is its front: `type: ignore` ends where the checker's own + # NOTE: word ends, and prose that runs on past it is not addressed to + # NOTE: the checker at all. + "# typeish: ignore", KEPT_AS_DIRECTIVE, - "# {}", ), "optimizer-hint": Sample( "sql", "oracle", - b"select /*+ index(t) */ 1 from dual; -- control\n", + f"select {SLOT} 1 from dual; -- control\n", + "/*+ index(t) */", + # NOTE: The `+` has to touch the `/*`, which is the whole of what makes + # NOTE: a hint a hint; a block comment that merely opens with one is an + # NOTE: ordinary comment about the index. + "/* + index(t) */", KEPT_AS_DIRECTIVE, - "-- {}", ), "version-comment": Sample( "sql", "mysql", - b"/*!40101 SET NAMES utf8 */ -- control\n", + f"{SLOT} -- control\n", + "/*!40101 SET NAMES utf8 */", + "/* !40101 SET NAMES utf8 */", KEPT_AS_DIRECTIVE, - "-- {}", ), "syntax=": Sample( "shell", None, - b"# syntax=docker/dockerfile:1\n# control\n", - KEPT_AS_DIRECTIVE, - "# {}", + f"{SLOT}\n# control\n", + "# syntax=docker/dockerfile:1", # NOTE: BuildKit writes the frontend straight after the `=`, so the # NOTE: marker carries its own boundary and `syntax=ish` is the # NOTE: directive naming a frontend that does not exist. - negative="a note about syntax=docker/dockerfile:1", + "# a note about syntax=docker/dockerfile:1", + KEPT_AS_DIRECTIVE, ), "hadolint": Sample( "shell", None, - b"# hadolint ignore=DL3018\n# control\n", + f"{SLOT}\n# control\n", + "# hadolint ignore=DL3018", + f"# hadolint{NEGATIVE_SUFFIX}", KEPT_AS_DIRECTIVE, - "# {}", ), } @@ -204,18 +259,26 @@ def protected_names() -> list[str]: return names -def negative_control(name: str, sample: Sample) -> str: - """The near-miss text this sample's marker must not protect.""" - return sample.negative if sample.negative is not None else f"{name}{NEGATIVE_SUFFIX}" +def marker_word(marker: str) -> str: + """The first word of a marker, past whatever punctuation opens it. + + `sourceMappingURL` of `//# sourceMappingURL=bundle.js.map`, `bin` of + `#!/bin/sh`, `coding` of `# -*- coding: utf-8 -*-`. A near-miss is checked + to have kept it, so one with nothing of the marker left in it -- the + `lint-and-formatterish note` this file used to derive from the category + name -- is refused rather than left to go on proving nothing. + """ + match = re.search(r"[A-Za-z_][A-Za-z_0-9-]*", marker) + return match.group() if match else "" -def scan(binary: pathlib.Path, language: str, dialect: str | None, source: bytes) -> list[dict]: - """Every comment the binary reports for one sample, in source order.""" - arguments = [str(binary), "scan", "--format", "json", "--language", language] - if dialect is not None: - arguments += ["--dialect", dialect] +def scan(binary: pathlib.Path, sample: Sample, comment: str) -> list[dict]: + """Every comment the binary reports for one built sample, in source order.""" + arguments = [str(binary), "scan", "--format", "json", "--language", sample.language] + if sample.dialect is not None: + arguments += ["--dialect", sample.dialect] completed = subprocess.run( - arguments + ["-"], input=source, check=True, capture_output=True + arguments + ["-"], input=sample.source(comment), check=True, capture_output=True ) document = json.loads(completed.stdout) return document["files"][0]["report"]["comments"] @@ -225,29 +288,44 @@ def check_sample(binary: pathlib.Path, name: str, failures: list[str]) -> None: """Run one sample and record what the binary said if it is not protection.""" sample = SAMPLES[name] where = f"`{name}` ({sample.language})" - negative = negative_control(name, sample) - source = sample.source + (sample.comment.format(negative) + "\n").encode() - comments = scan(binary, sample.language, sample.dialect, source) - if len(comments) != 3: - failures.append(f"{where}: {len(comments)} comments found, expected 3: {source!r}") + word = marker_word(sample.marker) + if word and word.lower() not in sample.near_miss.lower(): + failures.append( + f"{where}: the near-miss `{sample.near_miss}` keeps no word of" + f" `{sample.marker}`, so it tests nothing about that marker" + ) + comments = scan(binary, sample, sample.marker) + if len(comments) != 2: + failures.append( + f"{where}: {len(comments)} comments found, expected 2:" + f" {sample.source(sample.marker)!r}" + ) return - protected, control, near_miss = comments + protected, control = comments disposition = protected["disposition"] if disposition.get("action") != "keep": - failures.append(f"{where}: {source!r} is {disposition}, expected a keep") + failures.append(f"{where}: `{sample.marker}` is {disposition}, expected a keep") elif disposition.get("reason") != sample.reason: failures.append( f"{where}: kept as {disposition.get('reason')!r}, expected {sample.reason!r}" ) if control["disposition"].get("action") != "remove": failures.append( - f"{where}: the ordinary comment after it was kept too," + f"{where}: the ordinary comment beside it was kept too," " so the run protected the file rather than the marker" ) - if near_miss["disposition"].get("action") != "remove": + near_miss = scan(binary, sample, sample.near_miss) + if len(near_miss) != 2: + failures.append( + f"{where}: {len(near_miss)} comments found in the near-miss, expected 2:" + f" {sample.source(sample.near_miss)!r}" + ) + return + if near_miss[0]["disposition"].get("action") != "remove": failures.append( - f"{where}: `{negative}` was kept, so the marker is matched as a bare" - " prefix and protects prose that merely opens with it" + f"{where}: `{sample.near_miss}` was kept in the marker's own place," + " so the marker is matched loosely enough to protect a comment that" + " is only about it" ) @@ -283,7 +361,7 @@ def main() -> int: return 1 print( f"{len(names)} protected directives in spec/directives.toml are recognised," - " and none of them protects its near-miss" + " and none of them protects the near-miss written in its place" ) return 0 diff --git a/tools/check_embedded_specs.py b/tools/check_embedded_specs.py index 36420f1..5d915a1 100644 --- a/tools/check_embedded_specs.py +++ b/tools/check_embedded_specs.py @@ -7,6 +7,7 @@ PAIRS = ( (ROOT / "spec/config.schema.json", ROOT / "rust/ocomment/assets/config.schema.json"), (ROOT / "spec/default-config.toml", ROOT / "rust/ocomment/assets/default-config.toml"), + (ROOT / "spec/languages.toml", ROOT / "rust/ocomment/assets/languages.toml"), (ROOT / "spec/ocomment-scanner.wit", ROOT / "rust/ocomment/assets/ocomment-scanner.wit"), ) diff --git a/tools/differential.py b/tools/differential.py index 391e0c3..0cd1e51 100755 --- a/tools/differential.py +++ b/tools/differential.py @@ -1,210 +1,276 @@ #!/usr/bin/env python3 -"""Run normalized JSONL fixtures against Rust and OCaml implementations.""" +"""Run the shared spec fixture corpus against the Rust and OCaml implementations. + +Every case comes from `spec/fixtures/v1/*.json`; this file holds no fixture +bytes of its own. Each case becomes one normalized JSONL request, and the two +responses must be equal byte for byte. A case that carries an `expect` block is +additionally checked against that block, so the corpus pins absolute behaviour +rather than only agreement. + + python3 tools/differential.py compare, and check every expectation + python3 tools/differential.py --record also record the missing expectations + +`--record` writes an `expect` block into every case that has none, but only +once the two implementations have agreed on every case, which is what makes a +recorded block a record of the specification rather than of one implementation. +It never overwrites a block that is already there: re-recording an intentional +change means deleting that case's `expect` block first, in the same commit that +argues for the change. +""" import base64 import json import pathlib import subprocess import sys +import unicodedata ROOT = pathlib.Path(__file__).resolve().parents[1] RUST = ROOT / "rust/target/debug/examples/ref_driver" OCAML = ROOT / "ocaml/_build/default/bin/main.exe" +CORPUS = ROOT / "spec/fixtures/v1" -CORPUS = json.loads((ROOT / "spec/fixtures/v1/builtins.json").read_text(encoding="utf-8")) -assert CORPUS["version"] == 1 -# INVARIANT: `builtins.json` carries one case per language and is loaded into a -# INVARIANT: dict keyed by that language, so a second case for a language the -# INVARIANT: corpus already covers would silently replace the first rather than -# INVARIANT: be run. The assertion below is what says so out loud; anything past -# INVARIANT: the first case for a language belongs in SPECIAL_FIXTURES. -FIXTURES = { - case["language"]: case["source_utf8"].encode("utf-8") - for case in CORPUS["cases"] -} -assert len(FIXTURES) == len(CORPUS["cases"]), "builtins.json holds one case per language" - -UNICODE_COLUMN_SAMPLE = "".join( - chr(value) - for value in range(0x80, 0x110000, 97) - if not 0xD800 <= value <= 0xDFFF -).encode("utf-8") - - -SPECIAL_FIXTURES = [ - ("rust-nested-raw", "rust", br'r#"// opaque"# /* outer /* inner */ end */\n// rustfmt::skip\n', {}), - ("rust-raw-c-string", "rust", b'cr#"inner " // opaque"#; // remove\n', {}), - ("rust-multiline-string", "rust", b'const A: &str = "a\n// opaque\nb"; // remove\n', {}), - ("ocaml-nested-quoted", "ocaml", br'{tag| (* opaque *) |tag} (* outer "*)" (* inner *) *)', {}), - ("ocaml-comment-quoted", "ocaml", br'(* outer {tag| *) opaque |tag} end *)', {}), - ("ocaml-long-quoted-id", "ocaml", b"{" + b"a" * 80 + b"|(* opaque *)|" + b"a" * 80 + b"} (* remove *)", {}), - ("invalid-ocaml-quoted", "ocaml", br'{tag| unterminated (* opaque *)', {}), - ("c-line-splice", "c", b"int x; /\\\n/ comment\\\ncontinued\nint y;", {}), - ("cpp-raw", "cpp", br'R"tag(/* opaque */ // opaque)tag" // remove', {}), - ("go-directives", "go", b"//go:build linux\n// +build linux\n//line generated.go:1\n// remove\n", {}), - ("java-unicode", "java", br"int x; \u002f\u002f comment\u000aint y;", {}), - ("java-unicode-surrogates", "java", br'String s = "\uD83D\uDE00 // opaque"; // remove', {}), - ("invalid-java-unicode", "java", br"int x = 1; \u00G0 // known", {}), - ("forced-invalid-java-unicode", "java", br"int x = 1; \u00G0 // known", {"force_invalid": True}), - ("java-text-block-escape", "java", b'String s = """\n\\""" // opaque\nend\n"""; // remove\n', {}), - ("javascript-goals", "javascript", br'''#!/usr/bin/env node -const r = /\/\/* opaque/; -const t = `literal // opaque ${1 /* remove */}`; -// remove -''', {}), - ("javascript-control-regex", "javascript", br"if (ready) /https?:\/\/example\.test/.test(value); // remove", {}), - ("javascript-brace-goals", "javascript", b"const ratio = {} / 2; // remove\nif (ready) {} /[/*]/.test(value); // remove\n", {}), - ("javascript-html-like-comments", "javascript", b"const x = 1; remove\nconst text = '", {"policy": "all", "layout": "columns"}), - ("non-utf8-bytes", "c", b"\xff/* remove */\x80\r\n", {}), - ("compact-layout", "c", b"left/* remove */right\n", {"layout": "compact"}), - ("invalid-cpp-raw", "cpp", br'R"tag(unterminated /* opaque */', {}), - ("invalid-shell-quote", "shell", b"echo 'unterminated", {}), - ("invalid-shell-heredoc", "shell", b"cat < str: `spec/languages.toml` is the canonical table. The `files:` pattern of the published pre-commit hooks is generated from it, `tools/check_hooks.py` fails when the two drift apart, and the table below is generated from the same file. -`ocomment languages` prints a shorter summary of it; this is the complete one. +The binary embeds that same file, so `ocomment languages` prints these rows in +columns and `ocomment languages --format json` prints them as JSON. A dialect changes the lexical rules rather than the file type: `--dialect mysql` is still SQL, and only that dialect treats `/*!40101 ... */` as @@ -245,6 +246,13 @@ def commands_page(cli: Cli) -> str: """ +NAMED_INTRO = """\ +A file whose extension decides nothing is looked up by its whole name, and a +file with no name at all — a script on standard input — is read from its `#!` +line. A name is matched without regard to case, and a shebang matches when the +interpreter name appears anywhere on the line.""" + + def languages_page() -> str: """`docs/languages.md`: the shared language table, rendered.""" with LANGUAGES.open("rb") as stream: @@ -264,9 +272,24 @@ def languages_page() -> str: "| --- | --- | --- |", ] for entry in entries: - suffixes = ", ".join(f"`.{item}`" for item in entry["extensions"]) + dialect_of = entry.get("extension_dialects", {}) + suffixes = ", ".join( + f"`.{item}` (`{dialect_of[item]}`)" if item in dialect_of else f"`.{item}`" + for item in entry["extensions"] + ) forms = ", ".join(f"`{item}`" for item in entry["dialects"]) lines.append(f"| `{cell(entry['name'])}` | {cell(suffixes)} | {cell(forms)} |") + lines.extend(["", "## Detected without an extension", ""]) + lines.extend(NAMED_INTRO.splitlines()) + lines.extend(["", "| Language | File names | Shebangs |", "| --- | --- | --- |"]) + for entry in entries: + names = ", ".join(f"`{item}`" for item in entry.get("reserved_names", ())) + shebangs = ", ".join(f"`{item}`" for item in entry.get("shebangs", ())) + if not names and not shebangs: + continue + lines.append( + f"| `{cell(entry['name'])}` | {cell(names) or '—'} | {cell(shebangs) or '—'} |" + ) lines.extend( [ "", @@ -381,18 +404,26 @@ def policies_page(cli: Cli, workspace: pathlib.Path) -> str: cwd=workspace, ) lines.extend(["", f"### `{layout}`", "", fence("text", outputs[layout])]) - # INVARIANT: the paragraph below tells the reader that every layout keeps the - # INVARIANT: line count of the file, which is the property that keeps a line - # INVARIANT: number in a stack trace pointing at the same statement. A layout - # INVARIANT: that stopped doing it has to fail here rather than ship a page - # INVARIANT: that says it still does. + # INVARIANT: the paragraph below tells the reader that `lines` and `columns` + # INVARIANT: keep the line count of the file, which is the property that keeps + # INVARIANT: a line number in a stack trace pointing at the same statement, and + # INVARIANT: that `compact` is the one layout that gives it up. A layout that + # INVARIANT: stopped doing either has to fail here rather than ship a page that + # INVARIANT: says it still does. expected_lines = POLICY_SAMPLE.count(chr(10)) for layout, output in outputs.items(): - if output.count(chr(10)) != expected_lines: + counted = output.count(chr(10)) + if layout == "compact": + if counted >= expected_lines: + raise SystemExit( + f"layout `compact` left all {expected_lines} lines standing;" + " docs/policies.md claims it drops the lines a removed comment" + " had to itself" + ) + elif counted != expected_lines: raise SystemExit( f"layout `{layout}` turned {expected_lines} lines into" - f" {output.count(chr(10))}; docs/policies.md claims the line count" - " is preserved" + f" {counted}; docs/policies.md claims the line count is preserved" ) same = [ f"`{first}` and `{second}`" @@ -409,13 +440,22 @@ def policies_page(cli: Cli, workspace: pathlib.Path) -> str: ) lines.extend( [ - "Every layout keeps the line count of the file: a comment that spanned", - "three lines is replaced by something that still spans three lines, so", - "a line number in a stack trace or a `git blame` still points at the", - "same statement. `columns` additionally keeps every following column in", - "place by padding with spaces, which is what a table of aligned", - "initialisers or a column-sensitive language wants, at the cost of", - "trailing whitespace that a formatter may then remove.", + "`lines` and `columns` keep the line count of the file: a comment that", + "spanned three lines is replaced by something that still spans three", + "lines, so a line number in a stack trace or a `git blame` still points", + "at the same statement. `columns` additionally keeps every following", + "column in place by padding with spaces, which is what a table of", + "aligned initialisers or a column-sensitive language wants, at the cost", + "of trailing whitespace that a formatter may then remove.", + "", + "`compact` is the layout that gives line numbers up. A line that held", + "nothing but a removed comment goes away with it, terminator included,", + "and the whitespace a removal would leave at the end of a line is", + "trimmed. Code keeps its own lines: a comment sharing a line with code", + "leaves that line, its terminator and its CRLF or LF style as they", + "were. A surviving line keeps the ending it had in the source - the", + "same LF or CRLF, from inside the comment if that is where it was - or", + "no ending at all if the file stopped there without one.", "", "## Where they are set", "", From 98f83fada29856777ab7a41a2f358e7cab86fe23 Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:02:26 +0900 Subject: [PATCH 11/21] feat(languages): TOML and Lua built-in scanners; Java doc markers; fuzz-found parity fixes - TOML: # comments, basic/literal/multi-line strings with the 3-of-5 quote rule and backslash continuation, quoted keys, Taplo #:schema / taplo: directives, reserved lock file names - Lua: -- and ---(LDoc) comments, level-matched long comments/strings ([==[ ]==]) via a reusable long-bracket helper, \z and other escapes, first-line #/#! handling, luacheck/selene/stylua/luacov and ---@diagnostic directives, lua/luajit shebangs - both mirrored in the OCaml reference with builtin + hazard spec cases and recorded expectations; detection, schemas, docs, pre-commit pattern, VS Code selector updated - Java: only /// and /** */ are documentation markers (Rust matched the OCaml reference); Python unterminated prefixed strings are anchored at the prefix - SARIF fixes report the compact edit region; strip/config refuse formats they cannot honour; five detector extensions published (cuh, hh, hxx, mlt, shtml); shebang table exposed and pinned against the spec; rustdoc lints the binary crate too Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RLSWPMQ9PvtGi3jyBF7bJx --- .github/workflows/ci.yml | 9 +- .ocomment.toml | 16 +- .pre-commit-hooks.yaml | 4 +- CHANGELOG.md | 23 +- CONTRIBUTING.md | 20 +- README.md | 6 +- docs/commands.md | 40 ++ docs/comparison.md | 2 +- docs/editors.md | 11 +- docs/languages.md | 14 +- docs/library.md | 5 +- docs/ocomment.1 | 4 + docs/why-kept.md | 7 + editors/vscode/CHANGELOG.md | 2 +- editors/vscode/README.md | 5 +- editors/vscode/package.json | 8 +- ocaml/lib/ocomment_ref.ml | 202 +++++- ocaml/lib/ocomment_ref.mli | 2 +- release-extras/_ocomment | 80 ++- release-extras/ocomment.1 | 4 + release-extras/ocomment.bash | 40 +- release-extras/ocomment.fish | 80 ++- rust/ocomment-core/src/detect.rs | 92 ++- rust/ocomment-core/src/lib.rs | 2 +- rust/ocomment-core/src/scanner.rs | 352 +++++++++- rust/ocomment-core/src/types.rs | 20 +- rust/ocomment-core/tests/languages.rs | 624 ++++++++++++++++- rust/ocomment-core/tests/names.rs | 4 +- rust/ocomment-core/tests/properties.rs | 15 + rust/ocomment/assets/config.schema.json | 2 +- rust/ocomment/assets/languages.toml | 59 +- rust/ocomment/src/cli.rs | 24 + rust/ocomment/src/output.rs | 47 +- rust/ocomment/src/values.rs | 2 + rust/ocomment/tests/cli.rs | 223 +++++- rust/ocomment/tests/spec_languages.rs | 42 +- spec/config.schema.json | 2 +- spec/directives.toml | 3 +- spec/fixtures/v1/builtins.json | 120 ++++ spec/fixtures/v1/hazards.json | 876 ++++++++++++++++++++++++ spec/languages.toml | 59 +- spec/result.schema.json | 2 +- tools/check_directives.py | 71 ++ tools/differential.py | 2 +- tools/gen_docs.py | 11 + 45 files changed, 3054 insertions(+), 184 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7f9398..df6746b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,10 +45,15 @@ jobs: rustdoc --test docs/library.md --edition 2024 \ --extern ocomment_core=rust/target/debug/libocomment_core.rlib \ -L rust/target/debug/deps - - name: The library documentation builds with no broken links + # The binary crate is in here for its links alone: nothing publishes its + # rustdoc, but its modules document each other, and a link that names a + # function somebody has since renamed is a wrong sentence wherever it is + # written. `missing_docs` stays off for it — a `clap` derive has no + # documentation to give. + - name: The documentation builds with no broken links env: RUSTDOCFLAGS: -D warnings - run: cargo doc --manifest-path rust/Cargo.toml --no-deps -p ocomment-core -p ocomment-plugin-sdk --locked + run: cargo doc --manifest-path rust/Cargo.toml --no-deps -p ocomment-core -p ocomment-plugin-sdk -p ocomment --locked - run: python3 tools/check_embedded_specs.py - run: python3 tools/check_hooks.py - run: python3 -m pip install --disable-pip-version-check jsonschema==4.25.1 diff --git a/.ocomment.toml b/.ocomment.toml index 8433db7..0c1b97e 100644 --- a/.ocomment.toml +++ b/.ocomment.toml @@ -1,17 +1,23 @@ -# OComment checks its own repository. `ocomment` from the root is the gate the -# `dogfood` CI job runs, and Lefthook runs `ocomment check --staged` before every -# commit; see CONTRIBUTING.md for the tag convention this configuration enforces. +# NOTE: OComment checks its own repository. `ocomment` from the root is the +# NOTE: gate the `dogfood` CI job runs, and Lefthook runs `ocomment check +# NOTE: --staged` before every commit; see CONTRIBUTING.md for the tag +# NOTE: convention this configuration enforces. TOML is a built-in language, so +# NOTE: this file is now one of the files that convention applies to. version = 1 [files] exclude = [ - # Vendored crates, fixture bytes, and packaging or benchmark scratch are not - # ours to rewrite: fixture comments are the test input itself. + # NOTE: Vendored crates, fixture bytes, and packaging or benchmark scratch are + # NOTE: not ours to rewrite: fixture comments are the test input itself. "rust/vendor/**", "spec/fixtures/**", "editors/vscode/test-fixtures/**", "release-extras/**", "benchmarks/**", + # NOTE: A lock file is written by its resolver rather than by hand, and the + # NOTE: header Cargo puts at the top of this one comes back on the next + # NOTE: `cargo update`. + "rust/Cargo.lock", ] [policy] diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 2cb49ef..8fffae9 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -11,7 +11,7 @@ entry: ocomment check language: system types: [text] - files: '(?i)\.(bash|c|cc|cjs|cpp|css|cts|cu|cxx|go|h|hpp|htm|html|java|js|json5|jsonc|jsx|kt|kts|m|mjs|ml|mli|mm|mts|py|pyi|pyw|rs|sh|sql|ts|tsx|xhtml|zsh)$' + files: '(?i)\.(bash|c|cc|cjs|cpp|css|cts|cu|cuh|cxx|go|h|hh|hpp|htm|html|hxx|java|js|json5|jsonc|jsx|kt|kts|lua|m|mjs|ml|mli|mlt|mm|mts|py|pyi|pyw|rockspec|rs|sh|shtml|sql|toml|ts|tsx|xhtml|zsh)$' - id: ocomment-fix name: ocomment fix description: 'Remove comments from the staged source files in place; exit 1 from the following ocomment-check run, or a file pre-commit sees modified, blocks the commit until the result is reviewed and staged.' @@ -19,4 +19,4 @@ language: system types: [text] require_serial: true - files: '(?i)\.(bash|c|cc|cjs|cpp|css|cts|cu|cxx|go|h|hpp|htm|html|java|js|json5|jsonc|jsx|kt|kts|m|mjs|ml|mli|mm|mts|py|pyi|pyw|rs|sh|sql|ts|tsx|xhtml|zsh)$' + files: '(?i)\.(bash|c|cc|cjs|cpp|css|cts|cu|cuh|cxx|go|h|hh|hpp|htm|html|hxx|java|js|json5|jsonc|jsx|kt|kts|lua|m|mjs|ml|mli|mlt|mm|mts|py|pyi|pyw|rockspec|rs|sh|shtml|sql|toml|ts|tsx|xhtml|zsh)$' diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f3f56..b54a905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to OComment will be documented here. The project follows ### Added -- Byte-oriented scanners and transformations for 15 built-in languages and the +- Byte-oriented scanners and transformations for 17 built-in languages and the documented dialects. - CLI, staged Git fixes, LSP 3.18 server, declarative profiles, and sandboxed WASM component plugins. @@ -119,6 +119,27 @@ All notable changes to OComment will be documented here. The project follows dialects against the list the binary prints when it refuses one, the schema enumerations against the same vocabulary, and both listings against the table itself. +- TOML is a built-in language, scanned by a lexer of its own rather than by the + profile engine. `#` opens the only comment form there is, and every string + form hides one: basic and literal strings, the multi-line forms of both — + where the closing delimiter is the last three of a run of up to five quotes — + and the quoted keys written in either. `.toml` selects it, as do the lock + files written in TOML that carry no extension of their own (`Cargo.lock`, + `Pipfile`, `poetry.lock`, `uv.lock`, `pdm.lock`; `Pipfile.lock` is JSON and + is not among them). Taplo's `#:schema` and `# taplo:` lines are directives a + removal keeps. +- Lua is a built-in language, scanned by a lexer of its own. `--` opens a short + comment and a long bracket after it — `--[[`, `--[==[` — a long one, which + ends only at the closing bracket of its own level; the same brackets without + the `--` are long strings, and `a[b[1]]` is neither, because a long bracket + needs its second `[`. Short strings carry `\z`, which swallows the whitespace + and newlines after it, and a backslash before a line ending, which carries it + into the string. `---` is the documentation comment of LDoc and the Lua + language server, a fourth dash makes an ordinary divider, and `---@diagnostic` + is a directive where the other annotations are documentation, alongside the + `-- luacheck:`, `-- selene:`, `-- stylua:` and `-- luacov:` lines. `.lua` and + `.rockspec` select it, as does a `lua` or `luajit` `#!` line — which, like any + first line that opens with `#`, the loader skips. ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 31e904d..dfba180 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,10 +141,24 @@ JSONL, SARIF, and GitHub output. All user-facing text is English. changes; `tools/check_embedded_specs.py` checks shared embedded assets. - Adding a language to `spec/languages.toml` also changes the published pre-commit hooks; `tools/check_hooks.py --print-pattern` regenerates the - `files:` regex that `.pre-commit-hooks.yaml` must carry. -- Adding a marker to `spec/directives.toml` needs a sample in + `files:` regex that `.pre-commit-hooks.yaml` must carry. It changes what this + repository checks about itself as well: a file the new scanner now reads is a + file whose comments have to carry a tag, so run a bare `ocomment` before + opening the change. Two more things count the languages rather than reading + the table: `MINIMUM_CASES` in `tools/differential.py`, the floor that stops a + later change from quietly dropping the fixtures the language brings, and the + editor clients — `editors/vscode/package.json` lists the identifiers the + extension attaches to, and `docs/editors.md` names and counts them. +- Adding a marker to `spec/directives.toml` needs two samples: one in `tools/check_directives.py`, which proves the scanner protects it — and, - through a near-miss derived from the name, that it protects nothing more. + through a near-miss derived from the name, that it protects nothing more — + and one in `PROTECTED_SAMPLES` of `tools/gen_docs.py`, which is the row the + published table of protected markers is generated from. Each is checked + against `spec/directives.toml` on its own, so a marker with only one of the + two fails the run that needs the other. Both run the binary where this + repository's own `.ocomment.toml` applies, so write the near-miss as a comment + kind that configuration does not keep: a `doc-line` one comes back kept for a + reason that has nothing to do with the marker under test. - A new CI job must be added to `.github/rulesets/main.json` in the same change, and every `uses:` must be SHA-pinned with a version comment. - Do not include build output, credentials, or unrelated formatting changes. diff --git a/README.md b/README.md index 0dc050d..815cb07 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ tool is the Rust `ocomment` binary and the public `ocomment-core` library. classification, diagnostics, edits, transformed bytes, and source maps. OComment supports Rust, OCaml, C, C++, Go, Java, JavaScript, TypeScript, Python, -Shell, HTML, CSS, JSONC, SQL, and Kotlin. JSX/TSX, Objective-C/C++, GNU C/C++, -CUDA, POSIX sh, Bash 5.3, zsh, PostgreSQL, MySQL, SQLite, T-SQL, and Oracle are -explicit dialects. HTML `\n\n", + "note": "ECMA-262 12.5: a hashbang comment opens a Script or a Module and nothing else, and OComment reads that as `a file`: a preamble is a preamble at absolute offset 0. The content of a `\n\n" + } + }, + { + "id": "yaml-hash-in-plain-scalar", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "url: http://example.test/page#fragment\nname: a#b\ndone: 1 # remove\n", + "note": "YAML 1.2.2, 6.6 (Comment): a comment must be separated from other tokens by white space, so the `#` of a URL fragment and the one inside the plain scalar `a#b` are content of those scalars and only the `#` behind a space opens a comment.", + "expect": { + "valid": true, + "comments": [ + { + "start": 57, + "end": 65, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "url: http://example.test/page#fragment\nname: a#b\ndone: 1 \n" + } + }, + { + "id": "yaml-hash-after-space", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "key: value # remove\nother: 2\t# remove too\n# a whole line\n", + "note": "YAML 1.2.2, 6.6 with 6.1 (Indentation Spaces): a space or a tab is the white space that separates a comment from the token in front of it, and a comment may also be the whole of a line.", + "expect": { + "valid": true, + "comments": [ + { + "start": 11, + "end": 19, + "kind": "line", + "action": "remove" + }, + { + "start": 29, + "end": 41, + "kind": "line", + "action": "remove" + }, + { + "start": 42, + "end": 56, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "key: value \nother: 2\t\n\n" + } + }, + { + "id": "yaml-double-quoted-multiline-hash", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "key: \"first # not a comment\n second # still not\"\ndone: 1 # remove\n", + "note": "YAML 1.2.2, 7.3.1 (Double-Quoted Style): a double-quoted scalar folds over a line break, so it runs on to its closing quote and every `#` between the two is content.", + "expect": { + "valid": true, + "comments": [ + { + "start": 58, + "end": 66, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "key: \"first # not a comment\n second # still not\"\ndone: 1 \n" + } + }, + { + "id": "yaml-single-quoted-escape", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "key: 'it''s # not a comment'\nplain: it's fine # remove\n", + "note": "YAML 1.2.2, 7.3.2 (Single-Quoted Style): `''` is how a single-quoted scalar carries a quote of its own, so the first of the pair does not close it. An apostrophe inside a plain scalar (7.3.3) opens no scalar at all, which is what leaves the comment after `it's fine` a comment.", + "expect": { + "valid": true, + "comments": [ + { + "start": 46, + "end": 54, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "key: 'it''s # not a comment'\nplain: it's fine \n" + } + }, + { + "id": "yaml-block-literal-body-hash", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "script: |\n # not a comment\n echo hi\ndone: 1 # remove\n", + "note": "YAML 1.2.2, 8.1.2 (Literal Style): the content of a literal scalar is the following lines indented past the node it hangs off, carried verbatim, so a `#` on one of them is one of its bytes.", + "expect": { + "valid": true, + "comments": [ + { + "start": 46, + "end": 54, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "script: |\n # not a comment\n echo hi\ndone: 1 \n" + } + }, + { + "id": "yaml-block-folded-indent-indicator", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "text: >2\n # not a comment\n still folded\ndone: 1 # remove\n", + "note": "YAML 1.2.2, 8.1.1.1 (Block Indentation Indicator): the indicator states the content indentation explicitly, counted from the parent node, so a body line one space deeper than that is still body and the `#` on it is content.", + "expect": { + "valid": true, + "comments": [ + { + "start": 51, + "end": 59, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "text: >2\n # not a comment\n still folded\ndone: 1 \n" + } + }, + { + "id": "yaml-block-header-comment", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "script: |- # remove\n # not a comment\ndone: 1\n", + "note": "YAML 1.2.2, 8.1.1 (Block Header): the header takes its indicators and then, behind white space, a comment of its own, so the `#` on the header line is a comment where the `#` on the body line below it is content.", + "expect": { + "valid": true, + "comments": [ + { + "start": 11, + "end": 19, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "script: |- \n # not a comment\ndone: 1\n" + } + }, + { + "id": "yaml-sequence-item-block-scalar", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "steps:\n - run: |\n echo hi # not a comment\n - run: echo bye # remove\n", + "note": "YAML 1.2.2, 8.2.1 (Block Sequences) with 8.1: the block scalar of a sequence entry hangs off the key inside that entry, so its body is what is indented past that key and the next entry, which is not, ends it.", + "expect": { + "valid": true, + "comments": [ + { + "start": 66, + "end": 74, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "steps:\n - run: |\n echo hi # not a comment\n - run: echo bye \n" + } + }, + { + "id": "yaml-block-ends-at-document-marker", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "|\n a # not a comment\n---\n# remove\n", + "note": "YAML 1.2.2, 9.1.2 (Document Markers) with 8.1: a block scalar that is the whole document has no parent indentation to fall short of, so what ends its body is `---` in column zero -- and the comment on the line after that is a comment.", + "expect": { + "valid": true, + "comments": [ + { + "start": 26, + "end": 34, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "|\n a # not a comment\n---\n\n" + } + }, + { + "id": "yaml-empty-lines-in-body", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "script: |\n first\n\n # not a comment\n\ndone: 1 # remove\n", + "note": "YAML 1.2.2, 8.1.2: literal content is a run of empty lines followed by an indented line, so an empty line does not end the scalar and the `#` below one is still content.", + "expect": { + "valid": true, + "comments": [ + { + "start": 46, + "end": 54, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "script: |\n first\n\n # not a comment\n\ndone: 1 \n" + } + }, + { + "id": "yaml-flow-collection-comment", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "flow: [a,\"b # no\", 'c # no'] # remove\nmap: {x: 1} # remove too\n", + "note": "YAML 1.2.2, 7.4 (Flow Collection Styles): entries are separated by `,` and need no white space around it, so a quote right behind one still opens a scalar; the comment rule of 6.6 is the same inside a flow collection as outside it.", + "expect": { + "valid": true, + "comments": [ + { + "start": 29, + "end": 37, + "kind": "line", + "action": "remove" + }, + { + "start": 50, + "end": 62, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "flow: [a,\"b # no\", 'c # no'] \nmap: {x: 1} \n" + } + }, + { + "id": "yaml-directive-lines", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "%YAML 1.2\n%TAG !e! tag:example.test,2000:app/\n---\nkey: 1 # remove\n", + "note": "YAML 1.2.2, 6.8 (Directives): a `%` directive line is not a comment, and neither is the `---` that ends the directives of a document; only the `#` on the last line opens one.", + "expect": { + "valid": true, + "comments": [ + { + "start": 57, + "end": 65, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "%YAML 1.2\n%TAG !e! tag:example.test,2000:app/\n---\nkey: 1 \n" + } + }, + { + "id": "yaml-language-server-directive", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "# yaml-language-server: $schema=https://example.test/schema.json\n# renovate: datasource=docker depName=alpine\nkey: 1 # remove\n", + "note": "The editor's YAML language server reads the schema out of `# yaml-language-server:` and Renovate reads the datasource out of `# renovate:`. Both are instructions to a tool, both are protected by `spec/directives.toml`, and the ordinary comment beside them is what a `safe` run removes.", + "expect": { + "valid": true, + "comments": [ + { + "start": 0, + "end": 64, + "kind": "directive", + "action": "keep" + }, + { + "start": 65, + "end": 109, + "kind": "directive", + "action": "keep" + }, + { + "start": 117, + "end": 125, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "# yaml-language-server: $schema=https://example.test/schema.json\n# renovate: datasource=docker depName=alpine\nkey: 1 \n" + } + }, + { + "id": "yaml-yamllint-directive", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "# yamllint disable-line rule:line-length\n# checkov:skip=CKV_AWS_20:public by design\n# @schema type: string\nkey: 1 # remove\n", + "note": "yamllint suppresses a rule for the next line, Checkov skips a policy for the resource below, and the Helm schema generator reads `# @schema` as the type of the value it annotates. All three are protected; the ordinary comment beside them is not.", + "expect": { + "valid": true, + "comments": [ + { + "start": 0, + "end": 40, + "kind": "directive", + "action": "keep" + }, + { + "start": 41, + "end": 83, + "kind": "directive", + "action": "keep" + }, + { + "start": 84, + "end": 106, + "kind": "directive", + "action": "keep" + }, + { + "start": 114, + "end": 122, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "# yamllint disable-line rule:line-length\n# checkov:skip=CKV_AWS_20:public by design\n# @schema type: string\nkey: 1 \n" + } + }, + { + "id": "yaml-crlf", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "key: \"first # no\r\n second\"\r\nscript: |\r\n # no\r\ndone: 1 # remove\r\n", + "note": "YAML 1.2.2, 5.4 (Line Break Characters): a CRLF pair is one line break, so a folded double-quoted scalar and a block scalar body each read it as one and the line endings come back unchanged.", + "expect": { + "valid": true, + "comments": [ + { + "start": 56, + "end": 64, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "key: \"first # no\r\n second\"\r\nscript: |\r\n # no\r\ndone: 1 \r\n" + } + }, + { + "id": "yaml-tabs", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "script: |\n \t# not a comment\n text\ndone: 1\t# remove\n", + "note": "YAML 1.2.2, 6.1 (Indentation Spaces): indentation is spaces alone and a tab is never part of it, so a tab behind the indentation of a body line is content of that body; 6.6 still counts a tab as the white space that separates a comment from a token.", + "expect": { + "valid": true, + "comments": [ + { + "start": 44, + "end": 52, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "script: |\n \t# not a comment\n text\ndone: 1\t\n" + } + }, + { + "id": "yaml-unterminated-double-quote", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "key: \"unclosed # not a comment\nother: 1 # not one either\n", + "note": "YAML 1.2.2, 7.3.1: nothing but the closing quote ends a double-quoted scalar, so the end of the file leaves this one unterminated, no `#` after it opens a comment, and the source is not edited.", + "expect": { + "valid": false, + "comments": [], + "diagnostics": [ + { + "code": "unterminated-string", + "start": 5, + "end": 57 + } + ], + "output_utf8": "key: \"unclosed # not a comment\nother: 1 # not one either\n" + } + }, + { + "id": "yaml-columns-layout", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "columns" + }, + "source_utf8": "key: 1 # remove\nnext: 2\n", + "note": "YAML 1.2.2, 6.6: a comment runs to the end of the line, and the `columns` layout pads what it removes so the following columns do not shift.", + "expect": { + "valid": true, + "comments": [ + { + "start": 7, + "end": 15, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "key: 1 \nnext: 2\n" + } + }, + { + "id": "yaml-compact-layout", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "# alone\nkey: 1 # trailing\nnext: 2\n", + "note": "YAML 1.2.2, 6.6: the `compact` layout drops a line that held only a removed comment, and the whitespace a trailing one left behind.", + "expect": { + "valid": true, + "comments": [ + { + "start": 0, + "end": 7, + "kind": "line", + "action": "remove" + }, + { + "start": 15, + "end": 25, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "key: 1\nnext: 2\n" + } + }, + { + "id": "yaml-block-scalar-sequence-entry", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "- |\n # a\n b\n", + "note": "YAML 1.2.2, 8.1.1.1: the body of `- |` hangs off the sequence, whose indentation is the column of the `-`, so both lines under the header are content and the `#` is a byte of the scalar.", + "expect": { + "valid": true, + "comments": [], + "diagnostics": [], + "output_utf8": "- |\n # a\n b\n" + } + }, + { + "id": "yaml-block-scalar-tag", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "key: !!str |\n # a\n", + "note": "YAML 1.2.2, 6.9: a tag is a node property standing in front of the node rather than being one, so the `|` behind it is still a block scalar header and the line under it is content.", + "expect": { + "valid": true, + "comments": [], + "diagnostics": [], + "output_utf8": "key: !!str |\n # a\n" + } + }, + { + "id": "yaml-block-scalar-anchor", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "key: &x |\n # a\n", + "note": "An anchor is a node property like a tag (6.9): the header behind it opens a block scalar whose body is content, not a plain scalar holding a `|`.", + "expect": { + "valid": true, + "comments": [], + "diagnostics": [], + "output_utf8": "key: &x |\n # a\n" + } + }, + { + "id": "yaml-block-scalar-explicit-key", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "? |\n # a\n: v\n", + "note": "YAML 1.2.2, 8.2.2: an explicit key `?` names the column its node hangs off exactly as `-` does, so the body under the header is content and the `: v` at column zero is the first line that ends it.", + "expect": { + "valid": true, + "comments": [], + "diagnostics": [], + "output_utf8": "? |\n # a\n: v\n" + } + }, + { + "id": "yaml-block-scalar-nested-sequence", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "- - |\n # a\n", + "note": "The owner is the inner `-`, so the body is what is indented past that column rather than past the outer one.", + "expect": { + "valid": true, + "comments": [], + "diagnostics": [], + "output_utf8": "- - |\n # a\n" + } + }, + { + "id": "yaml-block-scalar-owner-depth", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "k:\n - |\n # a\n # still body\n # end\n", + "note": "The body ends at the first non-empty line that is not indented past the owner. The `-` sits at column 2, so column 3 is still body and column 2 is the comment that ends the scalar -- and that comment is the line the body would swallow back if a removal left anything on it, so every layout takes the whole line. Under `columns` the two spaces the line already had plus the five the comment was wide reach column 7, well past the body floor at column 3.", + "expect": { + "valid": true, + "comments": [ + { + "start": 35, + "end": 40, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k:\n - |\n # a\n # still body\n" + } + }, + { + "id": "yaml-block-scalar-indentation-indicator", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "k: |2\n # body\n", + "note": "YAML 1.2.2, 8.1.1.1: an explicit indentation indicator counts from the owner, so `|2` under a key at column zero puts the content floor at column 2 and the line at column 3 is content.", + "expect": { + "valid": true, + "comments": [], + "diagnostics": [], + "output_utf8": "k: |2\n # body\n" + } + }, + { + "id": "yaml-block-scalar-document-root", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "|\n # body\n", + "note": "A block scalar that is the whole document hangs off the root, one short of column zero, so every line under it is content whatever its indentation.", + "expect": { + "valid": true, + "comments": [], + "diagnostics": [], + "output_utf8": "|\n # body\n" + } + }, + { + "id": "yaml-block-scalar-header-own-line", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "key:\n |\n # a\n", + "note": "YAML 1.2.2, 8.2.2 with 8.1.1.1: the header may sit on the line under the key that owns it, as deep as it likes. The body is measured from the key, so a body shallower than the header's own column is still content.", + "expect": { + "valid": true, + "comments": [], + "diagnostics": [], + "output_utf8": "key:\n |\n # a\n" + } + }, + { + "id": "yaml-block-scalar-properties-previous-line", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "key: !!str\n |\n # a\n", + "note": "A node property may be separated from its node by a line break (6.9), so the owner carried across that break is still the key and the body under the header is content.", + "expect": { + "valid": true, + "comments": [], + "diagnostics": [], + "output_utf8": "key: !!str\n |\n # a\n" + } + }, + { + "id": "yaml-block-scalar-root-properties", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "!!str |\n # a\n", + "note": "A tag in front of the document's own node leaves the header a header, and the body it opens still hangs off the root.", + "expect": { + "valid": true, + "comments": [], + "diagnostics": [], + "output_utf8": "!!str |\n # a\n" + } + }, + { + "id": "yaml-keep-chomp-comment-after-body-lines", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "k: |+\n body\n\n# after\nnext: 1 # yes\n", + "note": "YAML 1.2.2, 8.1.1.2: `|+` keeps every empty line trailing a block scalar as part of its value, while the comment line under that body is `l-trail-comments` and is not. Blanking the comment would hand the `+` a line of content it did not have, so the removal takes the whole line, its terminator included, which is the one hole `lines` does not leave open.", + "expect": { + "valid": true, + "comments": [ + { + "start": 14, + "end": 21, + "kind": "line", + "action": "remove" + }, + { + "start": 30, + "end": 35, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: |+\n body\n\nnext: 1 \n" + } + }, + { + "id": "yaml-keep-chomp-comment-after-body-columns", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "columns" + }, + "source_utf8": "k: |+\n body\n\n# after\nnext: 1 # yes\n", + "note": "YAML 1.2.2, 8.1.1.2: `|+` keeps every empty line trailing a block scalar as part of its value, while the comment line under that body is `l-trail-comments` and is not. Blanking the comment would hand the `+` a line of content it did not have, so the removal takes the whole line, its terminator included. `columns` gives up that one line rather than the value: a line of spaces where the comment was is content of a keep-chomped scalar just as a blank one is.", + "expect": { + "valid": true, + "comments": [ + { + "start": 14, + "end": 21, + "kind": "line", + "action": "remove" + }, + { + "start": 30, + "end": 35, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: |+\n body\n\nnext: 1 \n" + } + }, + { + "id": "yaml-keep-chomp-comment-after-body-compact", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "k: |+\n body\n\n# after\nnext: 1 # yes\n", + "note": "YAML 1.2.2, 8.1.1.2: `|+` keeps every empty line trailing a block scalar as part of its value, while the comment line under that body is `l-trail-comments` and is not. Blanking the comment would hand the `+` a line of content it did not have, so the removal takes the whole line, its terminator included, which is the line `compact` takes for every comment that was alone on one.", + "expect": { + "valid": true, + "comments": [ + { + "start": 14, + "end": 21, + "kind": "line", + "action": "remove" + }, + { + "start": 30, + "end": 35, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: |+\n body\n\nnext: 1\n" + } + }, + { + "id": "yaml-keep-chomp-trail-swallows-sheltered-blanks-lines", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "all", + "layout": "lines" + }, + "source_utf8": "k: |+\n a\n# c\n\nz: 1\n", + "note": "YAML 1.2.2, 8.1.1.2: `l-keep-empty` is the empty lines trailing a body followed by `l-trail-comments`, so the blank line under this comment is separation while the comment is there and content of the `|+` once it is gone. Taking the comment's line alone would grow the scalar from `a\\n` to `a\\n\\n`; the removal takes the run of empty lines the comment was sheltering with it.", + "expect": { + "valid": true, + "comments": [ + { + "start": 10, + "end": 13, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: |+\n a\nz: 1\n" + } + }, + { + "id": "yaml-keep-chomp-trail-swallows-sheltered-blanks-columns", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "all", + "layout": "columns" + }, + "source_utf8": "k: |+\n a\n# c\n\nz: 1\n", + "note": "YAML 1.2.2, 8.1.1.2: `l-keep-empty` is the empty lines trailing a body followed by `l-trail-comments`, so the blank line under this comment is separation while the comment is there and content of the `|+` once it is gone. Taking the comment's line alone would grow the scalar from `a\\n` to `a\\n\\n`; the removal takes the run of empty lines the comment was sheltering with it.", + "expect": { + "valid": true, + "comments": [ + { + "start": 10, + "end": 13, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: |+\n a\nz: 1\n" + } + }, + { + "id": "yaml-keep-chomp-trail-swallows-sheltered-blanks-compact", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "all", + "layout": "compact" + }, + "source_utf8": "k: |+\n a\n# c\n\nz: 1\n", + "note": "YAML 1.2.2, 8.1.1.2: `l-keep-empty` is the empty lines trailing a body followed by `l-trail-comments`, so the blank line under this comment is separation while the comment is there and content of the `|+` once it is gone. Taking the comment's line alone would grow the scalar from `a\\n` to `a\\n\\n`; the removal takes the run of empty lines the comment was sheltering with it.", + "expect": { + "valid": true, + "comments": [ + { + "start": 10, + "end": 13, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: |+\n a\nz: 1\n" + } + }, + { + "id": "yaml-keep-chomp-blank-above-the-trail-stays-lines", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "all", + "layout": "lines" + }, + "source_utf8": "k: |+\n a\n\n# c\n\nz: 1\n", + "note": "The other half of the same rule. The empty line *above* the first trail comment is already `l-keep-empty` content of the `|+` -- the value is `a\\n\\n` before the removal and has to stay `a\\n\\n` after it -- so a removal takes what the comment was sheltering below it and never reaches back over what it was not.", + "expect": { + "valid": true, + "comments": [ + { + "start": 11, + "end": 14, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: |+\n a\n\nz: 1\n" + } + }, + { + "id": "yaml-keep-chomp-blank-above-the-trail-stays-columns", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "all", + "layout": "columns" + }, + "source_utf8": "k: |+\n a\n\n# c\n\nz: 1\n", + "note": "The other half of the same rule. The empty line *above* the first trail comment is already `l-keep-empty` content of the `|+` -- the value is `a\\n\\n` before the removal and has to stay `a\\n\\n` after it -- so a removal takes what the comment was sheltering below it and never reaches back over what it was not.", + "expect": { + "valid": true, + "comments": [ + { + "start": 11, + "end": 14, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: |+\n a\n\nz: 1\n" + } + }, + { + "id": "yaml-keep-chomp-blank-above-the-trail-stays-compact", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "all", + "layout": "compact" + }, + "source_utf8": "k: |+\n a\n\n# c\n\nz: 1\n", + "note": "The other half of the same rule. The empty line *above* the first trail comment is already `l-keep-empty` content of the `|+` -- the value is `a\\n\\n` before the removal and has to stay `a\\n\\n` after it -- so a removal takes what the comment was sheltering below it and never reaches back over what it was not.", + "expect": { + "valid": true, + "comments": [ + { + "start": 11, + "end": 14, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: |+\n a\n\nz: 1\n" + } + }, + { + "id": "yaml-clip-chomp-trail-comment-takes-its-line-lines", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "all", + "layout": "lines" + }, + "source_utf8": "k: |\n a\n# c\n\nz: 1\n", + "note": "Clip chomping drops the empty lines trailing a body, so the blank line `lines` would leave here changes nothing -- but the line of spaces `columns` would leave is indented to column 3, past the body floor at column 2, and is body content. One rule covers both: a whole-line comment under any block scalar body goes with its line under every layout, and the blank line below it stays because nothing was sheltering it.", + "expect": { + "valid": true, + "comments": [ + { + "start": 9, + "end": 12, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: |\n a\n\nz: 1\n" + } + }, + { + "id": "yaml-clip-chomp-trail-comment-takes-its-line-columns", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "all", + "layout": "columns" + }, + "source_utf8": "k: |\n a\n# c\n\nz: 1\n", + "note": "Clip chomping drops the empty lines trailing a body, so the blank line `lines` would leave here changes nothing -- but the line of spaces `columns` would leave is indented to column 3, past the body floor at column 2, and is body content. One rule covers both: a whole-line comment under any block scalar body goes with its line under every layout, and the blank line below it stays because nothing was sheltering it.", + "expect": { + "valid": true, + "comments": [ + { + "start": 9, + "end": 12, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: |\n a\n\nz: 1\n" + } + }, + { + "id": "yaml-clip-chomp-trail-comment-takes-its-line-compact", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "all", + "layout": "compact" + }, + "source_utf8": "k: |\n a\n# c\n\nz: 1\n", + "note": "Clip chomping drops the empty lines trailing a body, so the blank line `lines` would leave here changes nothing -- but the line of spaces `columns` would leave is indented to column 3, past the body floor at column 2, and is body content. One rule covers both: a whole-line comment under any block scalar body goes with its line under every layout, and the blank line below it stays because nothing was sheltering it.", + "expect": { + "valid": true, + "comments": [ + { + "start": 9, + "end": 12, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: |\n a\n\nz: 1\n" + } + }, + { + "id": "yaml-plain-scalar-pipe-opens-no-trail-lines", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "all", + "layout": "lines" + }, + "source_utf8": "k: a |+\n# c\n\nz: 1\n", + "note": "YAML 1.2.2, 8.1.1: a block scalar header opens a node, and no node begins after the `a` of a plain scalar, so these two bytes are content and there is no body below them. Reading them as a header would hang a keep-chomped trail off a line that has none and take the line of a comment sheltering nothing, so the comment is removed the ordinary way and keeps its line.", + "expect": { + "valid": true, + "comments": [ + { + "start": 8, + "end": 11, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: a |+\n\n\nz: 1\n" + } + }, + { + "id": "yaml-plain-scalar-pipe-opens-no-trail-columns", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "all", + "layout": "columns" + }, + "source_utf8": "k: a |+\n# c\n\nz: 1\n", + "note": "YAML 1.2.2, 8.1.1: a block scalar header opens a node, and no node begins after the `a` of a plain scalar, so these two bytes are content and there is no body below them. Reading them as a header would hang a keep-chomped trail off a line that has none and take the line of a comment sheltering nothing, so the comment is removed the ordinary way and keeps its line.", + "expect": { + "valid": true, + "comments": [ + { + "start": 8, + "end": 11, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: a |+\n \n\nz: 1\n" + } + }, + { + "id": "yaml-plain-scalar-pipe-opens-no-trail-compact", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "all", + "layout": "compact" + }, + "source_utf8": "k: a |+\n# c\n\nz: 1\n", + "note": "YAML 1.2.2, 8.1.1: a block scalar header opens a node, and no node begins after the `a` of a plain scalar, so these two bytes are content and there is no body below them. Reading them as a header would hang a keep-chomped trail off a line that has none and take the line of a comment sheltering nothing, so the comment is removed the ordinary way and keeps its line.", + "expect": { + "valid": true, + "comments": [ + { + "start": 8, + "end": 11, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "k: a |+\n\nz: 1\n" + } + }, + { + "id": "yaml-keep-chomp-surviving-comment-shelters-the-rest-lines", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "k: |+\n a\n# c\n\n# yamllint disable\n\nz: 1\n", + "note": "`safe` keeps the linter directive, so it stays as `l-trail-comments` and goes on sheltering the empty line below it. The removed comment above it still takes the empty line it was sheltering: without that the directive's own line would move down onto a blank the `+` had already claimed, and the value would grow from `a\\n` to `a\\n\\n`.", + "expect": { + "valid": true, + "comments": [ + { + "start": 10, + "end": 13, + "kind": "line", + "action": "remove" + }, + { + "start": 15, + "end": 33, + "kind": "directive", + "action": "keep" + } + ], + "diagnostics": [], + "output_utf8": "k: |+\n a\n# yamllint disable\n\nz: 1\n" + } + }, + { + "id": "yaml-keep-chomp-surviving-comment-shelters-the-rest-columns", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "columns" + }, + "source_utf8": "k: |+\n a\n# c\n\n# yamllint disable\n\nz: 1\n", + "note": "`safe` keeps the linter directive, so it stays as `l-trail-comments` and goes on sheltering the empty line below it. The removed comment above it still takes the empty line it was sheltering: without that the directive's own line would move down onto a blank the `+` had already claimed, and the value would grow from `a\\n` to `a\\n\\n`.", + "expect": { + "valid": true, + "comments": [ + { + "start": 10, + "end": 13, + "kind": "line", + "action": "remove" + }, + { + "start": 15, + "end": 33, + "kind": "directive", + "action": "keep" + } + ], + "diagnostics": [], + "output_utf8": "k: |+\n a\n# yamllint disable\n\nz: 1\n" + } + }, + { + "id": "yaml-keep-chomp-surviving-comment-shelters-the-rest-compact", + "language": "yaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "compact" + }, + "source_utf8": "k: |+\n a\n# c\n\n# yamllint disable\n\nz: 1\n", + "note": "`safe` keeps the linter directive, so it stays as `l-trail-comments` and goes on sheltering the empty line below it. The removed comment above it still takes the empty line it was sheltering: without that the directive's own line would move down onto a blank the `+` had already claimed, and the value would grow from `a\\n` to `a\\n\\n`.", + "expect": { + "valid": true, + "comments": [ + { + "start": 10, + "end": 13, + "kind": "line", + "action": "remove" + }, + { + "start": 15, + "end": 33, + "kind": "directive", + "action": "keep" + } + ], + "diagnostics": [], + "output_utf8": "k: |+\n a\n# yamllint disable\n\nz: 1\n" + } + }, + { + "id": "parity-js-html-close-behind-a-byte-order-mark", + "language": "javascript", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_base64": "Cu+7vy0tPiBjb21tZW50CnggLS0+IG5vdCBvbmUK", + "note": "ECMA-262 12.2 lists U+FEFF among WhiteSpace and 12.5 makes a `-->` that nothing but white space precedes on its line a SingleLineHTMLCloseComment, so a byte order mark in front of one leaves it a comment. On the line below a token precedes the `-->`, which is a decrement and a comparison there and opens nothing.", + "expect": { + "valid": true, + "comments": [ + { + "start": 4, + "end": 15, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_base64": "Cu+7vwp4IC0tPiBub3Qgb25lCg==" + } + }, + { + "id": "parity-js-html-close-behind-a-mark-that-is-not-the-first-byte", + "language": "javascript", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_base64": "CiDvu78tLT4gY29tbWVudAo=", + "note": "U+FEFF is white space wherever it sits, not only at the first byte of a file or of a line, so a space in front of the mark does not stop the `-->` behind it from closing a comment.", + "expect": { + "valid": true, + "comments": [ + { + "start": 5, + "end": 16, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_base64": "CiDvu78K" + } + }, + { + "id": "parity-ocaml-comment-character-literal-shape", + "language": "ocaml", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "(*'\\cr#\"]'*)\n", + "note": "OCaml manual, Lexical conventions: a comment lexes the string and character literals inside it, and a character literal is an apostrophe, one character or one escape sequence, and a closing apostrophe. `\\c` is no escape sequence and no apostrophe closes it, so the apostrophe here is a byte of the comment's text -- which leaves the `\"` behind it opening a string that never terminates, and the comment unterminated with it. Nothing is edited.", + "expect": { + "valid": false, + "comments": [ + { + "start": 0, + "end": 19, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [ + { + "code": "unterminated-comment", + "start": 0, + "end": 19 + } + ], + "output_utf8": "(*'\\cr#\"]'*)\n" + } + }, + { + "id": "php-html-then-php", + "language": "php", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "

#not a comment

\n#not a comment

\n\n", + "note": "PHP manual, Basic syntax: `\n" + } + }, + { + "id": "php-xml-decl-not-open-tag", + "language": "php", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "\n\n

kept

\n", + "note": "PHP manual, Comments: a one-line comment ends at the end of the line or at the closing tag, whichever comes first, and the `?>` is not part of it, so the markup behind it is inline HTML again rather than more comment.", + "expect": { + "valid": true, + "comments": [ + { + "start": 6, + "end": 16, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "

kept

\n" + } + }, + { + "id": "php-close-tag-swallows-newline", + "language": "php", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "\n#!/usr/bin/env php\n\n#!/usr/bin/env php\n not html\"; $b = '?>'; // remove\n", + "note": "A `?>` between the quotes of either string form is bytes of that string; only one the scanner meets in code leaves PHP mode, so the file never returns to inline HTML here.", + "expect": { + "valid": true, + "comments": [ + { + "start": 37, + "end": 46, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": " not html\"; $b = '?>'; \n" + } + }, + { + "id": "php-shebang", + "language": "php", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "#!/usr/bin/env php\n\r\n

x

\r\n", + "note": "Every construct that runs over a line break reads a CRLF pair as the one line ending it is: the one-line comment stops in front of it, the block comment carries it, the heredoc header and terminator recognise it, and the closing tag takes the pair away as a single break.", + "expect": { + "valid": true, + "comments": [ + { + "start": 6, + "end": 13, + "kind": "line", + "action": "remove" + }, + { + "start": 15, + "end": 32, + "kind": "block", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "\r\n

x

\r\n" + } + }, + { + "id": "php-unterminated-heredoc", + "language": "php", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": " bytes: f"-- luacov{NEGATIVE_SUFFIX}", KEPT_AS_DIRECTIVE, ), + "yaml-language-server:": Sample( + "yaml", + None, + f"{SLOT}\n# control\n", + "# yaml-language-server: $schema=https://example.test/schema.json", + # NOTE: The colon is the marker's own boundary, so letters run on past + # NOTE: it are still an instruction to the editor's YAML server. What is + # NOTE: left to get wrong is the front of it, which is what a comment + # NOTE: merely mentioning the server takes away. + "# a note about yaml-language-server: $schema", + KEPT_AS_DIRECTIVE, + ), + "yamllint": Sample( + "yaml", + None, + f"{SLOT}\n# control\n", + "# yamllint disable-line rule:line-length", + f"# yamllint{NEGATIVE_SUFFIX}", + KEPT_AS_DIRECTIVE, + ), + "renovate:": Sample( + "yaml", + None, + f"{SLOT}\n# control\n", + "# renovate: datasource=docker depName=alpine", + "# a note about renovate: datasource", + KEPT_AS_DIRECTIVE, + ), + "checkov:skip": Sample( + "yaml", + None, + f"{SLOT}\n# control\n", + "# checkov:skip=CKV_AWS_20:public by design", + # NOTE: Checkov writes the rule straight after the `=`, so the marker + # NOTE: carries its own boundary and what is left to get wrong is again + # NOTE: whether it opens the comment. + "# a note about checkov:skip=CKV_AWS_20", + KEPT_AS_DIRECTIVE, + ), + "trivy:ignore": Sample( + "yaml", + None, + f"{SLOT}\n# control\n", + "# trivy:ignore:AVD-AWS-0089", + "# a note about trivy:ignore:AVD-AWS-0089", + KEPT_AS_DIRECTIVE, + ), + "nosec": Sample( + "yaml", + None, + f"{SLOT}\n# control\n", + "# nosec", + f"# nosec{NEGATIVE_SUFFIX}", + KEPT_AS_DIRECTIVE, + ), + "kics-scan": Sample( + "yaml", + None, + f"{SLOT}\n# control\n", + "# kics-scan ignore-line", + f"# kics-scan{NEGATIVE_SUFFIX}", + KEPT_AS_DIRECTIVE, + ), + "phpcs:": Sample( + "php", + None, + f"", + "--", "---", "#", "#!", "--[[", "]]", "--[=[", "]=]", "[[", "[=[", +] +LINE_STRUCTURE = ["\n", "\r\n", "\r", " ", " ", " ", "\t", "\x0b", "\x0c"] +QUOTES_AND_ESCAPES = [ + '"', "'", "`", "\\", "\\\\", '"""', "'''", "r\"", "r#\"", "\"#", "cr#\"", + "br\"", "R\"(", ")\"", "u8\"", "L'", "$$", "N'", "@\"", "$tag$", "e\"", + "q'[", "]'", "\\z", "\\x41", "\\u{41}", "\\ddd", "\\\n", +] +CODE_BYTES = [ + "{", "}", "(", ")", "[", "]", ";", "=", "+", "-", "<", ">", "/", "*", + "!", "?", ":", ".", ",", "a", "b", "x", "r", "let", "fn", "def", "class", + "SELECT", "$", "%", "&", "|", "^", "~", "0x", "1", "'a", "&'a", +] +TRANSLATION_PHASE = ["\\u0041", "\\u002f", "\\u005c", "u0027", "\\\r\n"] +DIRECTIVE_WORDS = [ + "rustfmt::skip", "shellcheck disable=SC1000", "shellcheck", "hadolint", + "eslint-disable", "prettier-ignore", "region", "endregion", "noqa", + ":schema", "taplo:", "luacheck:", "---@diagnostic", "go:generate", + "yamllint", "@schema", "yaml-language-server:", + "Copyright (c) 2020", "SPDX-License-Identifier: MIT", "@license", "NOTE:", + "coding: utf-8", "pragma once", +] +MARKUP = [ + "", "", "", + "
", "
", "", +] +SHELL_STRUCTURE = ["<-", "|2-", "|+", "\n ", "%YAML 1.2", "...", + "key:\n", "\n-\n", "!!str ", "&a ", +] + +# NOTE: The shapes that reach PHP mode at all. Nothing but a whole `", " 1: + options["dialect"] = rng.choice(dialects) + if rng.random() < 0.1: + options["force_invalid"] = True + if rng.random() < 0.1: + options["force_protected"] = True + return options + + +def run(executable, requests): + """Feed a batch to one implementation and parse the responses.""" + payload = "".join(json.dumps(item, separators=(",", ":")) + "\n" for item in requests) + completed = subprocess.run( + [str(executable)], input=payload, text=True, capture_output=True, check=True + ) + return [json.loads(line) for line in completed.stdout.splitlines()] + + +def compare(requests): + """Every request of the batch whose two responses differ, as + `(request, rust, ocaml)`.""" + rust = run(RUST, requests) + ocaml = run(OCAML, requests) + if not len(rust) == len(ocaml) == len(requests): + raise SystemExit( + f"response count {len(rust)} (rust) vs {len(ocaml)} (ocaml) " + f"for {len(requests)} request(s)" + ) + return [ + (item, left, right) + for item, left, right in zip(requests, rust, ocaml) + if left != right + ] + + +def difference_paths(left, right, prefix=""): + """Where two responses differ, as dotted paths with list indices collapsed. + + Collapsing the indices is what makes a signature: the same bug reached from + a hundred sources names the same fields, however many comments happened to + precede the one it went wrong on. + """ + if type(left) is not type(right): + return [f"{prefix}:type"] + if isinstance(left, dict): + paths = [] + for key in sorted(set(left) | set(right)): + if key not in left or key not in right: + paths.append(f"{prefix}.{key}:absent") + else: + paths.extend(difference_paths(left[key], right[key], f"{prefix}.{key}")) + return paths + if isinstance(left, list): + if len(left) != len(right): + return [f"{prefix}:length"] + paths = [] + for item, other in zip(left, right): + paths.extend(difference_paths(item, other, f"{prefix}[]")) + return sorted(set(paths)) + return [] if left == right else [prefix] + + +def signature(language, left, right): + """What tells one divergence from another: the language, the fields that + disagree, and -- where the field is a message or a kind -- the two values, + because those name the rule that went wrong.""" + paths = tuple(sorted(set(difference_paths(left, right)))) + named = [] + for path in paths: + if path.endswith((".message", ".kind", ".code", ".action", ".reason")): + named.append((path, extract(left, path), extract(right, path))) + return (language, paths, tuple(named)) + + +def extract(value, path): + """The value at a dotted path, or `None` where the path does not lead + anywhere -- a list index was collapsed, or a key is absent.""" + for step in path.lstrip(".").split("."): + if step.endswith("[]"): + step = step[:-2] + if not isinstance(value, dict) or step not in value: + return None + value = value[step] + if not isinstance(value, list) or not value: + return None + value = value[0] + elif isinstance(value, dict) and step in value: + value = value[step] + else: + return None + return value + + +def shrink(item, tokens, language, budget): + """A shorter token list that still diverges, by dropping one token at a time. + + Greedy and bounded per signature: a divergence is meant to be rare, and the + point of the repro is to be short enough to paste into a fixture `note`, + not to be minimal. Every pass over the list is repeated until one removes + nothing, so a token that only became removable after its neighbour went is + still reached. + """ + current = list(tokens) + probes = budget + changed = True + while changed and probes > 0: + changed = False + index = 0 + while index < len(current) and probes > 0: + candidate = current[:index] + current[index + 1 :] + if not candidate: + break + probes -= 1 + probe = request(item["id"], language, assemble(candidate), item["options"], + item["operation"]) + if compare([probe]): + current = candidate + changed = True + else: + index += 1 + # NOTE: The two answers are read back from the shrunken source, so what the + # NOTE: report prints is what the source it prints really produces. + probe = request(item["id"], language, assemble(current), item["options"], + item["operation"]) + _, left, right = compare([probe])[0] + return assemble(current), left, right + + +def sweep(seed, cases, languages): + """Every divergence one seed turns up, keyed by signature.""" + rng = random.Random(seed) + requests = [] + sources = {} + for language, dialects in languages: + for index in range(cases): + tokens = random_tokens(rng) + identifier = f"{language}-{seed}-{index}" + sources[identifier] = tokens + requests.append( + request( + identifier, + language, + assemble(tokens), + random_options(rng, dialects), + rng.choice(OPERATIONS), + ) + ) + return requests, sources, compare(requests) + + +def main(argv): + parser = argparse.ArgumentParser( + description="Fuzz the Rust engine against the OCaml reference.", + epilog="An on-demand check. What it finds belongs in spec/fixtures/v1/hazards.json.", + ) + parser.add_argument( + "--seed", type=int, action="append", metavar="N", + help="a seed to sweep with; repeat for more than one (default: 1)", + ) + parser.add_argument( + "--cases", type=int, default=DEFAULT_CASES, metavar="N", + help=f"random sources per language per seed (default: {DEFAULT_CASES})", + ) + parser.add_argument( + "--shrink-probes", type=int, default=400, metavar="N", + help="single-token removals the shrinker may try per signature (default: 400)", + ) + arguments = parser.parse_args(argv) + for executable in (RUST, OCAML): + if not executable.exists(): + parser.error(f"{executable.relative_to(ROOT)} is not built; see the module docstring") + seeds = arguments.seed or [1] + languages = built_in_languages() + + found = {} + total = 0 + diverged = 0 + for seed in seeds: + requests, sources, divergences = sweep(seed, arguments.cases, languages) + total += len(requests) + diverged += len(divergences) + for item, left, right in divergences: + key = signature(item["language"], left.get("ok", left), right.get("ok", right)) + if key in found and len(assemble(sources[item["id"]])) >= len(found[key][0]): + continue + repro, shrunk_left, shrunk_right = shrink( + item, sources[item["id"]], item["language"], arguments.shrink_probes + ) + if key not in found or len(repro) < len(found[key][0]): + found[key] = (repro, item, shrunk_left, shrunk_right) + + print( + f"{total} request(s) over {len(seeds)} seed(s) x {len(languages)} language(s) " + f"x {arguments.cases} source(s); {diverged} divergent, " + f"{len(found)} distinct signature(s)" + ) + for key, (repro, item, left, right) in sorted(found.items(), key=lambda entry: str(entry[0])): + print("=" * 78) + print(f"language={item['language']} operation={item['operation']} options={item['options']}") + print(f"fields ={' '.join(key[1])}") + print(f"source ={repro!r}") + print(f"rust ={json.dumps(left.get('ok', left), sort_keys=True)[:800]}") + print(f"ocaml ={json.dumps(right.get('ok', right), sort_keys=True)[:800]}") + return 1 if found else 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/tools/gen_docs.py b/tools/gen_docs.py index bc63418..a275c35 100644 --- a/tools/gen_docs.py +++ b/tools/gen_docs.py @@ -90,6 +90,26 @@ "selene:": ("lua", None, b"-- selene: allow(unused_variable)\n"), "stylua:": ("lua", None, b"-- stylua: ignore\n"), "luacov:": ("lua", None, b"-- luacov: disable\n"), + "yaml-language-server:": ( + "yaml", + None, + b"# yaml-language-server: $schema=https://example.test/schema.json\n", + ), + "yamllint": ("yaml", None, b"# yamllint disable-line rule:line-length\n"), + "renovate:": ("yaml", None, b"# renovate: datasource=docker depName=alpine\n"), + "checkov:skip": ("yaml", None, b"# checkov:skip=CKV_AWS_20:public by design\n"), + "trivy:ignore": ("yaml", None, b"# trivy:ignore:AVD-AWS-0089\n"), + "nosec": ("yaml", None, b"# nosec\n"), + "kics-scan": ("yaml", None, b"# kics-scan ignore-line\n"), + "@schema": ("yaml", None, b"# @schema type: string\n"), + "phpcs:": ("php", None, b" str: "scanned as JavaScript and the contents of a `\n\n"; + let report = scan(source, Language::Vue, ScanOptions::default()); + assert!(report.valid, "diagnostics: {:?}", report.diagnostics); + assert_eq!( + report + .comments + .iter() + .map(|comment| (comment.span.start, comment.span.end, comment.kind)) + .collect::>(), + vec![ + (25, 30, CommentKind::Line), + (49, 58, CommentKind::Block), + (99, 106, CommentKind::Line), + ] + ); +} + +/// A `lang` this scanner has no rules for makes the block opaque: a +/// `\n\n\n"; + let report = scan(source, Language::Vue, ScanOptions::default()); + assert!(report.valid, "diagnostics: {:?}", report.diagnostics); + assert!(report.comments.is_empty(), "{:?}", report.comments); +} + +/// The `v-pre` directive makes an element's content raw text, so the mustache +/// it holds is not code and the `//` in it is not a comment. +/// +/// Ground truth, `@vue/compiler-sfc` 3.5: `
{{ x // c }}
` +/// parses with the whole content as one text node. +#[test] +fn vue_v_pre_elements_are_opaque() { + let source = b"
{{ x // not }}
\n\n"; + let report = scan(source, Language::Vue, ScanOptions::default()); + assert!(report.valid, "diagnostics: {:?}", report.diagnostics); + assert_eq!(report.comments.len(), 1, "{:?}", report.comments); + assert_eq!(report.comments[0].span, ByteSpan::new(43, 56)); + assert_eq!(report.comments[0].kind, CommentKind::HtmlComment); +} + +/// Vue is detected from the `.vue` of a single-file component. +#[test] +fn vue_is_detected_from_its_extension() { + let found = detect_language(Some(Path::new("App.vue")), b"\n") + .expect("detected by extension"); + assert_eq!(found.language, Language::Vue); + assert_eq!(found.reason, "extension"); +} + +/// A Svelte component's template is HTML with code in its braces: every +/// `{ ... }` opens an expression whose comments are comments — a line one runs +/// to the end of its line — and `` is an HTML comment. +/// +/// Ground truth, `svelte/compiler` 5.56: the source below parses with the +/// `/* c */` and `// d` as comments of their expressions and the HTML comment +/// as a comment node. +#[test] +fn svelte_expressions_and_comments_in_the_template() { + let source = b"

{x /* c */}

\n\n

{y // d\n}

\n"; + let report = scan(source, Language::Svelte, ScanOptions::default()); + assert!(report.valid, "diagnostics: {:?}", report.diagnostics); + assert_eq!( + report + .comments + .iter() + .map(|comment| (comment.span.start, comment.span.end, comment.kind)) + .collect::>(), + vec![ + (6, 13, CommentKind::Block), + (19, 32, CommentKind::HtmlComment), + (39, 43, CommentKind::Line), + ] + ); +} + +/// A Svelte component's `\n\n"; + let report = scan(source, Language::Svelte, ScanOptions::default()); + assert!(report.valid, "diagnostics: {:?}", report.diagnostics); + assert_eq!( + report + .comments + .iter() + .map(|comment| (comment.span.start, comment.span.end, comment.kind)) + .collect::>(), + vec![(19, 24, CommentKind::Line), (55, 62, CommentKind::Line),] + ); +} + +/// Svelte is detected from the `.svelte` of a component. +#[test] +fn svelte_is_detected_from_its_extension() { + let found = detect_language(Some(Path::new("App.svelte")), b"

x

\n") + .expect("detected by extension"); + assert_eq!(found.language, Language::Svelte); + assert_eq!(found.reason, "extension"); +} + +/// The three layouts leave a Vue file a line, columns, or nothing. +#[test] +fn vue_layouts_leave_a_line_columns_or_nothing() { + let source = b"\n"; + let options = TransformOptions { + scan: ScanOptions { + policy: Policy::All, + ..Default::default() + }, + ..Default::default() + }; + let lines = transform(source, Language::Vue, options); + assert_eq!(lines.output, b"\n"); + let compact = transform( + source, + Language::Vue, + TransformOptions { + layout: Layout::Compact, + scan: ScanOptions { + policy: Policy::All, + ..Default::default() + }, + }, + ); + assert_eq!(compact.output, b"\n"); +} diff --git a/rust/ocomment-core/tests/names.rs b/rust/ocomment-core/tests/names.rs index a6ae057..3e6c117 100644 --- a/rust/ocomment-core/tests/names.rs +++ b/rust/ocomment-core/tests/names.rs @@ -72,7 +72,7 @@ macro_rules! check_stable_names { #[test] fn language_names_are_stable() { let seen = check_stable_names!(Language); - assert_eq!(Language::ALL.len(), 26); + assert_eq!(Language::ALL.len(), 28); assert!( !seen.contains("unknown"), "Unknown must stay out of the parseable set" @@ -86,7 +86,7 @@ fn language_names_are_stable() { #[test] fn dialect_names_are_stable() { check_stable_names!(Dialect); - assert_eq!(Dialect::ALL.len(), 16); + assert_eq!(Dialect::ALL.len(), 17); } #[test] @@ -171,6 +171,8 @@ fn language_aliases_are_pinned() { ("cs", Language::CSharp), ("c#", Language::CSharp), ("scala", Language::Scala), + ("vue", Language::Vue), + ("svelte", Language::Svelte), ]; for (text, expected) in cases { assert_eq!(Language::from_str(text), Ok(expected), "`{text}`"); diff --git a/rust/ocomment/assets/config.schema.json b/rust/ocomment/assets/config.schema.json index 3a34826..58ebffb 100644 --- a/rust/ocomment/assets/config.schema.json +++ b/rust/ocomment/assets/config.schema.json @@ -70,7 +70,7 @@ "$defs": { "strings": { "type": "array", "items": { "type": "string" }, "default": [] }, "policy": { "enum": ["safe", "legal", "all"], "default": "safe" }, - "language": { "enum": ["rust", "ocaml", "c", "cpp", "go", "java", "javascript", "typescript", "python", "shell", "html", "css", "jsonc", "sql", "kotlin", "toml", "lua", "yaml", "php", "ruby", "zig", "r", "dart", "swift", "csharp", "scala"] }, + "language": { "enum": ["rust", "ocaml", "c", "cpp", "go", "java", "javascript", "typescript", "python", "shell", "html", "css", "jsonc", "sql", "kotlin", "toml", "lua", "yaml", "php", "ruby", "zig", "r", "dart", "swift", "csharp", "scala", "vue", "svelte"] }, "kind": { "enum": ["line", "block", "doc-line", "doc-block", "directive", "license", "html-comment", "shebang", "encoding", "optimizer-hint", "version-comment"] }, "kinds": { "type": "array", "items": { "$ref": "#/$defs/kind" }, "default": [] }, "languageConfig": { @@ -97,7 +97,7 @@ } }, "dialect": { - "enum": ["standard", "jsx", "tsx", "objective-c", "objective-cpp", "gnu-c", "gnu-cpp", "cuda", "posix-sh", "bash53", "zsh", "postgresql", "mysql", "sqlite", "t-sql", "oracle"] + "enum": ["standard", "jsx", "tsx", "objective-c", "objective-cpp", "gnu-c", "gnu-cpp", "cuda", "posix-sh", "bash53", "zsh", "postgresql", "mysql", "sqlite", "t-sql", "oracle", "scss"] }, "nonEmptyStrings": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } diff --git a/rust/ocomment/assets/languages.toml b/rust/ocomment/assets/languages.toml index 70337fb..d2161bf 100644 --- a/rust/ocomment/assets/languages.toml +++ b/rust/ocomment/assets/languages.toml @@ -95,8 +95,9 @@ notes = "script and style bodies are scanned as JavaScript and CSS" [[languages]] name = "css" -extensions = ["css"] -dialects = ["standard"] +extensions = ["css", "scss", "sass"] +dialects = ["standard", "scss"] +extension_dialects = { scss = "scss", sass = "scss" } [[languages]] name = "jsonc" @@ -226,3 +227,15 @@ extensions = ["scala", "sc"] dialects = ["standard"] shebangs = ["scala-cli", "scala"] notes = "Scala 3.8 lexical forms; block comments nest, an identifier before a quote makes the string interpolate, and an XML literal is opaque text with `{...}` code" + +[[languages]] +name = "vue" +extensions = ["vue"] +dialects = ["standard"] +notes = "Vue single-file components; the template is HTML with `{{ ... }}` code and the script and style bodies are their own languages, the `lang` attribute choosing which" + +[[languages]] +name = "svelte" +extensions = ["svelte"] +dialects = ["standard"] +notes = "Svelte components; the template is HTML with `{ ... }` code and the script and style bodies are their own languages, the `lang` attribute choosing which" diff --git a/rust/ocomment/src/config.rs b/rust/ocomment/src/config.rs index 66f0978..3f9b4a8 100644 --- a/rust/ocomment/src/config.rs +++ b/rust/ocomment/src/config.rs @@ -695,6 +695,7 @@ pub fn supported_dialects(language: Language) -> &'static [Dialect] { Dialect::GnuCpp, Dialect::Cuda, ], + Language::Css => &[Dialect::Standard, Dialect::Scss], Language::Shell => &[ Dialect::Standard, Dialect::PosixSh, diff --git a/rust/ocomment/src/values.rs b/rust/ocomment/src/values.rs index 4ff0f0d..cd41f06 100644 --- a/rust/ocomment/src/values.rs +++ b/rust/ocomment/src/values.rs @@ -130,6 +130,8 @@ value_enum_wrapper!(LanguageArg, Language, |value| match value { Language::CSharp => "C# source and script files, whose `#` lines are preprocessor directives", Language::Scala => "Scala source and script files, whose block comments nest and whose XML literals are opaque", + Language::Vue => "Vue single-file components, whose templates are HTML with `{{ ... }}` code", + Language::Svelte => "Svelte components, whose templates are HTML with `{ ... }` code", Language::Unknown => "An undetected language", }); @@ -150,6 +152,7 @@ value_enum_wrapper!(DialectArg, Dialect, |value| match value { Dialect::Sqlite => "SQLite", Dialect::TSql => "Microsoft Transact-SQL", Dialect::Oracle => "Oracle SQL and PL/SQL", + Dialect::Scss => "SCSS and the indented Sass syntax", }); value_enum_wrapper!(CommentKindArg, CommentKind, |value| match value { diff --git a/rust/ocomment/tests/cli.rs b/rust/ocomment/tests/cli.rs index 10435e4..dcc2420 100644 --- a/rust/ocomment/tests/cli.rs +++ b/rust/ocomment/tests/cli.rs @@ -5922,3 +5922,47 @@ fn a_scala_file_keeps_its_directive_and_hides_xml_text() { b"//> using scala \"3.3.0\"\nval a = // text\nval b = s\"${1 }\" \n\n" ); } + +/// A Vue file scans its template, script and style blocks, keeps the +/// template's HTML comment, and removes a comment from the mustache and from +/// each embedded language. +#[test] +fn a_vue_file_scans_its_template_script_and_style_blocks() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("App.vue"); + fs::write( + &path, + b"\n\n\n", + ) + .unwrap(); + + let scanned = run(directory.path(), &["scan", "App.vue", "--format", "json"]); + assert_eq!( + scanned.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&scanned.stderr) + ); + let document: serde_json::Value = serde_json::from_slice(&scanned.stdout).unwrap(); + let report = &document["files"][0]["report"]; + assert_eq!(report["language"], "vue"); + assert_eq!(report["comments"].as_array().unwrap().len(), 4); + assert_eq!(report["comments"][0]["kind"], "html-comment"); + assert_eq!(report["comments"][0]["disposition"]["action"], "keep"); + assert_eq!(report["comments"][1]["span"]["start"], 35); + assert_eq!(report["comments"][1]["span"]["end"], 42); + assert_eq!(report["comments"][2]["span"]["start"], 89); + assert_eq!(report["comments"][3]["span"]["start"], 125); + + let fixed = run(directory.path(), &["fix", "App.vue"]); + assert_eq!( + fixed.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&fixed.stderr) + ); + assert_eq!( + fs::read(&path).unwrap(), + b"\n\n\n" + ); +} diff --git a/spec/config.schema.json b/spec/config.schema.json index 3a34826..58ebffb 100644 --- a/spec/config.schema.json +++ b/spec/config.schema.json @@ -70,7 +70,7 @@ "$defs": { "strings": { "type": "array", "items": { "type": "string" }, "default": [] }, "policy": { "enum": ["safe", "legal", "all"], "default": "safe" }, - "language": { "enum": ["rust", "ocaml", "c", "cpp", "go", "java", "javascript", "typescript", "python", "shell", "html", "css", "jsonc", "sql", "kotlin", "toml", "lua", "yaml", "php", "ruby", "zig", "r", "dart", "swift", "csharp", "scala"] }, + "language": { "enum": ["rust", "ocaml", "c", "cpp", "go", "java", "javascript", "typescript", "python", "shell", "html", "css", "jsonc", "sql", "kotlin", "toml", "lua", "yaml", "php", "ruby", "zig", "r", "dart", "swift", "csharp", "scala", "vue", "svelte"] }, "kind": { "enum": ["line", "block", "doc-line", "doc-block", "directive", "license", "html-comment", "shebang", "encoding", "optimizer-hint", "version-comment"] }, "kinds": { "type": "array", "items": { "$ref": "#/$defs/kind" }, "default": [] }, "languageConfig": { @@ -97,7 +97,7 @@ } }, "dialect": { - "enum": ["standard", "jsx", "tsx", "objective-c", "objective-cpp", "gnu-c", "gnu-cpp", "cuda", "posix-sh", "bash53", "zsh", "postgresql", "mysql", "sqlite", "t-sql", "oracle"] + "enum": ["standard", "jsx", "tsx", "objective-c", "objective-cpp", "gnu-c", "gnu-cpp", "cuda", "posix-sh", "bash53", "zsh", "postgresql", "mysql", "sqlite", "t-sql", "oracle", "scss"] }, "nonEmptyStrings": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } diff --git a/spec/fixtures/v1/builtins.json b/spec/fixtures/v1/builtins.json index 324c95a..c4faff9 100644 --- a/spec/fixtures/v1/builtins.json +++ b/spec/fixtures/v1/builtins.json @@ -1542,7 +1542,90 @@ "diagnostics": [], "output_utf8": "val a = \"\"\"a\"\"\"\"\nval b = s\"\"\"${1 \n} \"\"\"\n\nval c = `a//b`\n\n" } + }, + { + "id": "vue-builtin-safe", + "language": "vue", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "\n\n\n", + "note": "Vue single-file components, @vue/compiler-sfc 3.5: the template's `` is an HTML comment that `safe` keeps as DOM-observable, `{{ ... }}` opens an expression whose `/* c */` is a comment, and the `\n\n" + } + }, + { + "id": "svelte-builtin-safe", + "language": "svelte", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "\n\n

{x /* c */}

\n\n", + "note": "Svelte components, svelte/compiler 5.56: the `\n\n

{x }

\n\n" + } } ] } - diff --git a/spec/fixtures/v1/floor.txt b/spec/fixtures/v1/floor.txt index 5a81c37..4d02082 100644 --- a/spec/fixtures/v1/floor.txt +++ b/spec/fixtures/v1/floor.txt @@ -16,5 +16,5 @@ # Blank lines and `#` lines are ignored; every other line is a name and a # decimal count separated by white space. -cases 456 -expectations 456 +cases 462 +expectations 462 diff --git a/spec/fixtures/v1/hazards.json b/spec/fixtures/v1/hazards.json index 6af4d41..37cefb0 100644 --- a/spec/fixtures/v1/hazards.json +++ b/spec/fixtures/v1/hazards.json @@ -904,7 +904,7 @@ "layout": "lines" }, "source_utf8": "x = r\"abc\ny = rb\"def\nz = r\"\"\"ghi\n", - "note": "Python reference 2.4.1: a string prefix and the quote after it are one token, so an unterminated literal is reported from the prefix and not from the quote — the same anchor for `r\"`, `rb\"` and `r\"\"\"`.", + "note": "Python reference 2.4.1: a string prefix and the quote after it are one token, so an unterminated literal is reported from the prefix and not from the quote \u2014 the same anchor for `r\"`, `rb\"` and `r\"\"\"`.", "expect": { "valid": false, "comments": [], @@ -1581,7 +1581,7 @@ "policy": "safe", "layout": "columns" }, - "source_utf8": "x\t/*中😀*/y\r\n", + "source_utf8": "x\t/*\u4e2d\ud83d\ude00*/y\r\n", "note": "Layout `columns` replaces a removed comment with spaces to the same display width; a tab, a wide CJK character, an emoji, and a CRLF ending must all survive unchanged.", "expect": { "valid": true, @@ -3410,7 +3410,7 @@ "policy": "safe", "layout": "lines" }, - "source_utf8": "let c = 'ä\\';\n", + "source_utf8": "let c = '\u00e4\\';\n", "note": "Rust Reference, Tokens (character literals): a character literal holds one character and ends at the line, so this one is never closed -- the `\\` in front of the second apostrophe carries it into the literal as an escape, and no third one arrives before the line break. It is also the shape that tells a literal from a lifetime -- a non-ASCII character with an apostrophe close enough behind it to be the closing quote -- which is why the diagnostic here is `unterminated character literal` and `parity-rust-lifetime-is-not-a-literal` reports nothing at all. Both apostrophes stand on one line because that lookahead stops at a line terminator (`rust-char-literal-across-newline`). Ground truth, `rustc` 1.97: `error[E0762]: unterminated character literal` for this line.", "expect": { "valid": false, @@ -3647,7 +3647,7 @@ } ], "diagnostics": [], - "output_utf8": "// region\n" + "output_utf8": "//\u00a0region\n" } }, { @@ -6999,7 +6999,7 @@ "layout": "lines" }, "source_utf8": "puts \"#{ <() {}` and a loop label in `'ä: loop { break 'ä }` exactly as `'a` does, and each of those is a valid file whose trailing comment must still be found. Ground truth, `rustc` 1.97: this file is rejected with `error[E0762]: unterminated character literal`, pointed at the apostrophe on line 2 rather than this one -- but E0762 is a parser judgement, reached after the lexer has read `'ä` as a lifetime and the parser has found that no lifetime may stand there. This scanner is a lexer with a line-bounded window and has no such judgement to make, so it reports nothing and holds the file valid: over-keeping on a file another tool will reject costs a rejected file one comment, and calling `fn f<'ä>() {}` invalid would cost a valid one its transformation. The reading of line 2 is unchanged either way -- its apostrophe opens nothing, and the comment behind it is found and removed.", + "source_utf8": "let c = '\u00e4\n'; // remove\n", + "note": "Rust Reference, Tokens: the lookahead that tells a character literal from a lifetime reads up to six bytes past the apostrophe, and it stops at a line terminator. The scanner offers a restart point at the line start behind every terminator, and a restart point promises that nothing decided before it depends on bytes after it, so a window that read across one would let an edit on line 2 rewrite a token on line 1 while an incremental rescan reused it unchanged. What the stop costs is the reading and no report at all, because within line 1 nothing separates an unterminated character literal from a Unicode lifetime: a Rust identifier is `XID_Start XID_Continue*` (Rust Reference, Identifiers) and has been since 1.53, so `'\u00e4` opens a lifetime in `fn f<'\u00e4>() {}` and a loop label in `'\u00e4: loop { break '\u00e4 }` exactly as `'a` does, and each of those is a valid file whose trailing comment must still be found. Ground truth, `rustc` 1.97: this file is rejected with `error[E0762]: unterminated character literal`, pointed at the apostrophe on line 2 rather than this one -- but E0762 is a parser judgement, reached after the lexer has read `'\u00e4` as a lifetime and the parser has found that no lifetime may stand there. This scanner is a lexer with a line-bounded window and has no such judgement to make, so it reports nothing and holds the file valid: over-keeping on a file another tool will reject costs a rejected file one comment, and calling `fn f<'\u00e4>() {}` invalid would cost a valid one its transformation. The reading of line 2 is unchanged either way -- its apostrophe opens nothing, and the comment behind it is found and removed.", "expect": { "valid": true, "comments": [ @@ -8922,7 +8922,7 @@ } ], "diagnostics": [], - "output_utf8": "let c = 'ä\n'; \n" + "output_utf8": "let c = '\u00e4\n'; \n" } }, { @@ -8933,8 +8933,8 @@ "policy": "safe", "layout": "lines" }, - "source_utf8": "fn f<'ä>() {} // remove\n", - "note": "Rust Reference, Identifiers: an identifier is `XID_Start XID_Continue*` and has been since 1.53, so `'ä` names a lifetime exactly as `'a` does. The line-bounded window that tells a character literal from a lifetime cannot tell those two apart -- within one line an unterminated non-ASCII character literal is spelled the same way -- so it reports neither, which is what leaves this file valid and its trailing comment removable. Ground truth, `rustc` 1.97.1: the file compiles, with `warning: function `f` is never used` and no error, so a scanner that called it invalid would refuse to transform a file the compiler accepts.", + "source_utf8": "fn f<'\u00e4>() {} // remove\n", + "note": "Rust Reference, Identifiers: an identifier is `XID_Start XID_Continue*` and has been since 1.53, so `'\u00e4` names a lifetime exactly as `'a` does. The line-bounded window that tells a character literal from a lifetime cannot tell those two apart -- within one line an unterminated non-ASCII character literal is spelled the same way -- so it reports neither, which is what leaves this file valid and its trailing comment removable. Ground truth, `rustc` 1.97.1: the file compiles, with `warning: function `f` is never used` and no error, so a scanner that called it invalid would refuse to transform a file the compiler accepts.", "expect": { "valid": true, "comments": [ @@ -8956,8 +8956,8 @@ "policy": "safe", "layout": "lines" }, - "source_utf8": "'ä: loop { break 'ä } // remove\n", - "note": "Rust Reference, Loop labels: a loop label is written with the lifetime-or-label token, so `'ä` labels a loop and breaks out of it exactly as `'a` would. Two apostrophes on one line each open nothing here, for the reason `rust-unicode-lifetime-generic` gives, and the comment behind them is still found. Ground truth, `rustc` 1.97.1: `fn main() { 'ä: loop { break 'ä } }` compiles with no error.", + "source_utf8": "'\u00e4: loop { break '\u00e4 } // remove\n", + "note": "Rust Reference, Loop labels: a loop label is written with the lifetime-or-label token, so `'\u00e4` labels a loop and breaks out of it exactly as `'a` would. Two apostrophes on one line each open nothing here, for the reason `rust-unicode-lifetime-generic` gives, and the comment behind them is still found. Ground truth, `rustc` 1.97.1: `fn main() { '\u00e4: loop { break '\u00e4 } }` compiles with no error.", "expect": { "valid": true, "comments": [ @@ -9310,7 +9310,7 @@ "layout": "lines" }, "source_utf8": "let a = \"x\"#/y // z/#\nlet b = 1 // remove\n", - "note": "The run of `#` that closes a string belongs to the string only when a run opened it: `Lexer.Cursor.advanceIfStringDelimiter` returns on `delimiterLength == 0` before it looks at a byte, so the `#` behind the closing quote of `\"x\"` opens the `#/ ... /#` that follows and the `//` inside that literal is pattern rather than a comment. Taking it for the string instead would report a comment over regular expression bytes and remove them. Ground truth, the SwiftSyntax parser of the Swift 6.3.3 toolchain (`SwiftParser.Parser.parse`, read for comment trivia and their UTF-8 offsets): `stringQuote` at [10,11), `regexPoundDelimiter` at [11,12), `regexLiteralPattern(\"y // z\")` at [13,19), and the only `lineComment` at [32,41). The juxtaposition is a parse error there — `consecutive statements on a line must be separated by newline or ';'` — and the lexing of it is what this case pins.", + "note": "The run of `#` that closes a string belongs to the string only when a run opened it: `Lexer.Cursor.advanceIfStringDelimiter` returns on `delimiterLength == 0` before it looks at a byte, so the `#` behind the closing quote of `\"x\"` opens the `#/ ... /#` that follows and the `//` inside that literal is pattern rather than a comment. Taking it for the string instead would report a comment over regular expression bytes and remove them. Ground truth, the SwiftSyntax parser of the Swift 6.3.3 toolchain (`SwiftParser.Parser.parse`, read for comment trivia and their UTF-8 offsets): `stringQuote` at [10,11), `regexPoundDelimiter` at [11,12), `regexLiteralPattern(\"y // z\")` at [13,19), and the only `lineComment` at [32,41). The juxtaposition is a parse error there \u2014 `consecutive statements on a line must be separated by newline or ';'` \u2014 and the lexing of it is what this case pins.", "expect": { "valid": true, "comments": [ @@ -9406,7 +9406,7 @@ "layout": "lines" }, "source_utf8": "let a = / b\\//\nlet c = 1\n", - "note": "The one rule of Swift's bare `/ ... /` literal that cannot be had from the bytes alone, recorded rather than hidden. Whether a `/` in an ambiguous position opens a literal is settled in Swift by the parser: where an expression is *required* the lexer takes the literal and diagnoses what is wrong with it, and where an operator would also parse it gives up instead. A scanner with no parser cannot tell the two apart, so this one reads a literal exactly where the book's own conditions hold — a prefix-operator position, no leading space or tab, no line terminator, a closing `/` that no unescaped blank precedes and no comment opener follows. Every source that decides differently is one `swiftc` rejects, and this is that source: Ground truth, the SwiftSyntax parser of the Swift 6.3.3 toolchain (`SwiftParser.Parser.parse`, read for comment trivia and their UTF-8 offsets) lexes `/ b\\\\//` as a `regexLiteralPattern` at [9,13) and reports `bare slash regex literal may not start with space`, while the scan below reads the `//` at [12,14) as a line comment. A file that compiles never reaches the case; a file that does not can lose the two bytes of a comment opener that the compiler was going to reject anyway.", + "note": "The one rule of Swift's bare `/ ... /` literal that cannot be had from the bytes alone, recorded rather than hidden. Whether a `/` in an ambiguous position opens a literal is settled in Swift by the parser: where an expression is *required* the lexer takes the literal and diagnoses what is wrong with it, and where an operator would also parse it gives up instead. A scanner with no parser cannot tell the two apart, so this one reads a literal exactly where the book's own conditions hold \u2014 a prefix-operator position, no leading space or tab, no line terminator, a closing `/` that no unescaped blank precedes and no comment opener follows. Every source that decides differently is one `swiftc` rejects, and this is that source: Ground truth, the SwiftSyntax parser of the Swift 6.3.3 toolchain (`SwiftParser.Parser.parse`, read for comment trivia and their UTF-8 offsets) lexes `/ b\\\\//` as a `regexLiteralPattern` at [9,13) and reports `bare slash regex literal may not start with space`, while the scan below reads the `//` at [12,14) as a line comment. A file that compiles never reaches the case; a file that does not can lose the two bytes of a comment opener that the compiler was going to reject anyway.", "expect": { "valid": true, "comments": [ @@ -9460,7 +9460,7 @@ "layout": "lines" }, "source_utf8": "let a = /x//y/\nlet b = 1\n", - "note": "When the closing delimiter of a bare literal would be the first byte of a comment, the comment wins outright rather than the literal ending one byte earlier: `RegexLiteralLexer.tryEatEnding` returns `.unterminated` on a `*` or `/` behind the closing slash, `\"We prefer to lex the comment as it's more likely than not that is what the user is expecting\"`. Ground truth, the SwiftSyntax parser of the Swift 6.3.3 toolchain (`SwiftParser.Parser.parse`, read for comment trivia and their UTF-8 offsets): no regex token, and one `lineComment` at [10,14) — `//y/` — with no parser diagnostic.", + "note": "When the closing delimiter of a bare literal would be the first byte of a comment, the comment wins outright rather than the literal ending one byte earlier: `RegexLiteralLexer.tryEatEnding` returns `.unterminated` on a `*` or `/` behind the closing slash, `\"We prefer to lex the comment as it's more likely than not that is what the user is expecting\"`. Ground truth, the SwiftSyntax parser of the Swift 6.3.3 toolchain (`SwiftParser.Parser.parse`, read for comment trivia and their UTF-8 offsets): no regex token, and one `lineComment` at [10,14) \u2014 `//y/` \u2014 with no parser diagnostic.", "expect": { "valid": true, "comments": [ @@ -9568,7 +9568,7 @@ "layout": "lines" }, "source_utf8": "// swift-format-ignore-file\n// control\n", - "note": "`swift-format` reads three spellings of its ignore comment — the bare marker, a `:` and a rule name, and the `-file` that widens it to the whole file. Measured on `swift-format` 6.3.3: `// swift-format-ignore` and `// swift-format-ignore-file` both leave `let a = 1` unformatted, while `// swift-format-ignoreish note` reformats it. Ground truth, the SwiftSyntax parser of the Swift 6.3.3 toolchain (`SwiftParser.Parser.parse`, read for comment trivia and their UTF-8 offsets): `lineComment` at [0,27) and [28,38).", + "note": "`swift-format` reads three spellings of its ignore comment \u2014 the bare marker, a `:` and a rule name, and the `-file` that widens it to the whole file. Measured on `swift-format` 6.3.3: `// swift-format-ignore` and `// swift-format-ignore-file` both leave `let a = 1` unformatted, while `// swift-format-ignoreish note` reformats it. Ground truth, the SwiftSyntax parser of the Swift 6.3.3 toolchain (`SwiftParser.Parser.parse`, read for comment trivia and their UTF-8 offsets): `lineComment` at [0,27) and [28,38).", "expect": { "valid": true, "comments": [ @@ -9711,7 +9711,7 @@ "layout": "lines" }, "source_utf8": "let a = 'x // not'\n// remove\n", - "note": "`'` is no delimiter of the language — the Swift book's Lexical Structure has no single-quoted literal and no character literal at all — but it is one in the compiler, which lexes `'...'` as a `singleQuote` string so that it can offer the fix-it that turns it into a `\"...\"` one. Following the lexer keeps the `//` inside such a literal from being removed out of a file that is already broken, and costs a valid file nothing, because no `'` can stand in Swift code outside a string, a comment or a regular expression literal. Ground truth, the SwiftSyntax parser of the Swift 6.3.3 toolchain (`SwiftParser.Parser.parse`, read for comment trivia and their UTF-8 offsets): one `stringSegment` at [9,17) and the only `lineComment` at [19,28), with the diagnostic `Single-quoted string literal found, use '\\\"'`.", + "note": "`'` is no delimiter of the language \u2014 the Swift book's Lexical Structure has no single-quoted literal and no character literal at all \u2014 but it is one in the compiler, which lexes `'...'` as a `singleQuote` string so that it can offer the fix-it that turns it into a `\"...\"` one. Following the lexer keeps the `//` inside such a literal from being removed out of a file that is already broken, and costs a valid file nothing, because no `'` can stand in Swift code outside a string, a comment or a regular expression literal. Ground truth, the SwiftSyntax parser of the Swift 6.3.3 toolchain (`SwiftParser.Parser.parse`, read for comment trivia and their UTF-8 offsets): one `stringSegment` at [9,17) and the only `lineComment` at [19,28), with the diagnostic `Single-quoted string literal found, use '\\\"'`.", "expect": { "valid": true, "comments": [ @@ -11049,7 +11049,107 @@ "diagnostics": [], "output_utf8": "val a = s\"x$\"y\"\nval b = s\"$$lit\"\n\n" } + }, + { + "id": "scss-protocol-relative-url", + "language": "css", + "dialect": "scss", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": ".b { background: url(//cdn/x.png) no-repeat }\n// yes\n", + "note": "dart-sass 1.93: an unquoted `url( ... )` is a URL even when it opens with `//` \u2014 `url(//cdn/x.png)` is a protocol-relative URL, not a silent comment \u2014 so the URL bytes are protected and only the `// yes` after the rule is a comment.", + "expect": { + "valid": true, + "comments": [ + { + "start": 46, + "end": 52, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": ".b { background: url(//cdn/x.png) no-repeat }\n\n" + } + }, + { + "id": "vue-v-pre-raw-text", + "language": "vue", + "operation": "scan", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "
{{ x // not }}
\n\n", + "note": "@vue/compiler-sfc 3.5: the `v-pre` directive makes an element's content raw text, so the mustache it holds is not code and the `//` in it is not a comment; the `` in the template is an HTML comment.", + "expect": { + "valid": true, + "comments": [ + { + "start": 43, + "end": 56, + "kind": "html-comment", + "action": "keep" + } + ], + "diagnostics": [] + } + }, + { + "id": "vue-unknown-embedded-language", + "language": "vue", + "operation": "scan", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "\n\n", + "note": "@vue/compiler-sfc 3.5 accepts a `", 'lang="ts"', 'lang="scss"', 'lang="less"', "v-pre", + "", ":title=", "// text", "/* c */", +] + +SVELTE_STRUCTURE = [ + "{", "}", "{#if", "{/if}", "{#each", "{/each}", "", + "", "", 'lang="ts"', 'lang="scss"', + "

", "

", "title=", "// text", "/* c */", +] + +SCSS_STRUCTURE = [ + "//", "/*", "*/", "#{", "}", "url(", ")", "$x:", 'content: "', "//cdn/", + "// c", "/* c */", "a {", "}", +] + # NOTE: The bytes a lexer is liable to mishandle: NUL, DEL, a byte order mark, a # NOTE: no-break space, the two Unicode line terminators, and two characters # NOTE: wider than one byte. @@ -223,6 +240,9 @@ + SWIFT_STRUCTURE + CSHARP_STRUCTURE + SCALA_STRUCTURE + + VUE_STRUCTURE + + SVELTE_STRUCTURE + + SCSS_STRUCTURE + AWKWARD_BYTES ) diff --git a/tools/gen_docs.py b/tools/gen_docs.py index 283a567..867d66b 100644 --- a/tools/gen_docs.py +++ b/tools/gen_docs.py @@ -555,6 +555,23 @@ def languages_page() -> str: "tab, line feed, `{`, `(` or `>` and followed by an XML name start,", "`!` or `?`.", "", + "Vue and Svelte components are HTML with code in the template: a", + "`` is an HTML comment, and `{{ ... }}` in Vue or", + "`{ ... }` in Svelte opens an expression whose comments are", + "comments — in Vue, a `v-pre` element makes its whole content raw", + "text instead. The `\n\n" ); } + +/// A Markdown file scans its fenced code blocks as their named languages and +/// keeps its HTML comment, while inline code stays opaque. +#[test] +fn a_markdown_file_scans_its_fenced_code_blocks() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("README.md"); + fs::write( + &path, + b"# notes\n\n```rust\n// c\n```\n`// inline`\n", + ) + .unwrap(); + + let scanned = run(directory.path(), &["scan", "README.md", "--format", "json"]); + assert_eq!( + scanned.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&scanned.stderr) + ); + let document: serde_json::Value = serde_json::from_slice(&scanned.stdout).unwrap(); + let report = &document["files"][0]["report"]; + assert_eq!(report["language"], "markdown"); + assert_eq!(report["comments"].as_array().unwrap().len(), 2); + assert_eq!(report["comments"][0]["kind"], "html-comment"); + assert_eq!(report["comments"][0]["disposition"]["action"], "keep"); + assert_eq!(report["comments"][1]["span"]["start"], 30); + assert_eq!(report["comments"][1]["span"]["end"], 34); + + let fixed = run(directory.path(), &["fix", "README.md"]); + assert_eq!( + fixed.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&fixed.stderr) + ); + assert_eq!( + fs::read(&path).unwrap(), + b"# notes\n\n```rust\n\n```\n`// inline`\n" + ); +} diff --git a/spec/config.schema.json b/spec/config.schema.json index 58ebffb..8cacf77 100644 --- a/spec/config.schema.json +++ b/spec/config.schema.json @@ -70,7 +70,7 @@ "$defs": { "strings": { "type": "array", "items": { "type": "string" }, "default": [] }, "policy": { "enum": ["safe", "legal", "all"], "default": "safe" }, - "language": { "enum": ["rust", "ocaml", "c", "cpp", "go", "java", "javascript", "typescript", "python", "shell", "html", "css", "jsonc", "sql", "kotlin", "toml", "lua", "yaml", "php", "ruby", "zig", "r", "dart", "swift", "csharp", "scala", "vue", "svelte"] }, + "language": { "enum": ["rust", "ocaml", "c", "cpp", "go", "java", "javascript", "typescript", "python", "shell", "html", "css", "jsonc", "sql", "kotlin", "toml", "lua", "yaml", "php", "ruby", "zig", "r", "dart", "swift", "csharp", "scala", "vue", "svelte", "markdown"] }, "kind": { "enum": ["line", "block", "doc-line", "doc-block", "directive", "license", "html-comment", "shebang", "encoding", "optimizer-hint", "version-comment"] }, "kinds": { "type": "array", "items": { "$ref": "#/$defs/kind" }, "default": [] }, "languageConfig": { diff --git a/spec/fixtures/v1/builtins.json b/spec/fixtures/v1/builtins.json index c4faff9..59dd37e 100644 --- a/spec/fixtures/v1/builtins.json +++ b/spec/fixtures/v1/builtins.json @@ -1626,6 +1626,36 @@ "diagnostics": [], "output_utf8": "\n\n

{x }

\n\n" } + }, + { + "id": "markdown-builtin-safe", + "language": "markdown", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "text\n\nmore\n```rust\n// c\n```\n`// inline`\n", + "note": "CommonMark 0.31: `` is an HTML block that `safe` keeps as DOM-observable, a ```rust fence is scanned as Rust so the `// c` inside is a comment, and an inline code span is opaque. Ground truth, the `commonmark` package: the comment parses as one `html_block` and the fence as a `code_block` with info `rust`.", + "expect": { + "valid": true, + "comments": [ + { + "start": 5, + "end": 18, + "kind": "html-comment", + "action": "keep" + }, + { + "start": 32, + "end": 36, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "text\n\nmore\n```rust\n\n```\n`// inline`\n" + } } ] } diff --git a/spec/fixtures/v1/floor.txt b/spec/fixtures/v1/floor.txt index 4d02082..2ec6beb 100644 --- a/spec/fixtures/v1/floor.txt +++ b/spec/fixtures/v1/floor.txt @@ -16,5 +16,5 @@ # Blank lines and `#` lines are ignored; every other line is a name and a # decimal count separated by white space. -cases 462 -expectations 462 +cases 464 +expectations 464 diff --git a/spec/fixtures/v1/hazards.json b/spec/fixtures/v1/hazards.json index 37cefb0..c92cf01 100644 --- a/spec/fixtures/v1/hazards.json +++ b/spec/fixtures/v1/hazards.json @@ -11150,6 +11150,22 @@ "diagnostics": [], "output_utf8": "

{x \n}

\n\n" } + }, + { + "id": "markdown-fences-and-inline-code", + "language": "markdown", + "operation": "scan", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "```nope\n// not a comment\n```\n`// not either`\n /* nor this */\n", + "note": "CommonMark 0.31: a fence whose info string names no language, an inline code span and an indented code block are all opaque \u2014 the `//` and `/*` inside them are code text, not comments \u2014 while a fence that names a language is scanned as it.", + "expect": { + "valid": true, + "comments": [], + "diagnostics": [] + } } ] } diff --git a/spec/languages.toml b/spec/languages.toml index d2161bf..a06d499 100644 --- a/spec/languages.toml +++ b/spec/languages.toml @@ -239,3 +239,9 @@ name = "svelte" extensions = ["svelte"] dialects = ["standard"] notes = "Svelte components; the template is HTML with `{ ... }` code and the script and style bodies are their own languages, the `lang` attribute choosing which" + +[[languages]] +name = "markdown" +extensions = ["md", "markdown", "rmd"] +dialects = ["standard"] +notes = "CommonMark documents; HTML comments are comments, fenced code blocks are scanned as the language their info string names, and inline and indented code are opaque" diff --git a/spec/result.schema.json b/spec/result.schema.json index 71deb2d..b904837 100644 --- a/spec/result.schema.json +++ b/spec/result.schema.json @@ -132,7 +132,7 @@ } }, "language": { - "enum": ["rust", "ocaml", "c", "cpp", "go", "java", "javascript", "typescript", "python", "shell", "html", "css", "jsonc", "sql", "kotlin", "toml", "lua", "yaml", "php", "ruby", "zig", "r", "dart", "swift", "csharp", "scala", "vue", "svelte", "unknown"] + "enum": ["rust", "ocaml", "c", "cpp", "go", "java", "javascript", "typescript", "python", "shell", "html", "css", "jsonc", "sql", "kotlin", "toml", "lua", "yaml", "php", "ruby", "zig", "r", "dart", "swift", "csharp", "scala", "vue", "svelte", "markdown", "unknown"] }, "kind": { "enum": ["line", "block", "doc-line", "doc-block", "directive", "license", "html-comment", "shebang", "encoding", "optimizer-hint", "version-comment"] diff --git a/tools/gen_docs.py b/tools/gen_docs.py index 867d66b..0f6a307 100644 --- a/tools/gen_docs.py +++ b/tools/gen_docs.py @@ -572,6 +572,12 @@ def languages_page() -> str: "ends them, a protocol-relative `url(//cdn/x.png)` included — with", "`#{ ... }` inside it code.", "", + "Markdown is scanned per CommonMark: an HTML comment is a comment,", + "a fenced code block is scanned as the language its info string", + "names — ```rust, `{r}` and `c++` all reach their scanners — and an", + "inline code span or an indented code block is opaque, so a `//` or", + "a `/*` inside one is code text, not a comment.", + "", "YAML is scanned lexically, and `valid` is a lexical answer: the", "shapes a YAML *parser* rejects are not all shapes a lexer can see.", "A comment line inside a multi-line plain scalar makes the file a", From 3109537ea2b4419d9fcada367b6d271b85ed123f Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:51:27 +0900 Subject: [PATCH 18/21] feat(languages): Perl, scanned conservatively with a lexical-ambiguity stop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `#` runs to the end of its line, a POD block is opaque, and every quote word — the single and double quotes and backticks, the `q`, `qq`, `qw` and `qx` forms, the `m`, `s`, `tr` and `y` operators with delimiters of their own, and the here-documents — hides a `#` written inside it. A `/` directly after a closing parenthesis, bracket or brace is reported as lexically ambiguous: perl reads `f() /a#b/` as a regular expression and `(2) / 2` as a division, and only the parse context tells which, so the file is called invalid and nothing is edited. Ground truth is perl 5.38, read with `perl -c` over the corpus cases. Checkpoint soundness: a checkpoint may no longer stand where an edit grew a `<` ... `>` tag across it, now asked by reading the tag forward from the `<` its walk meets, quotes included; and the incremental engine re-asks the converged tail's checkpoints against the edited document, exactly as it re-asks a candidate restart. The Rust engine and the OCaml reference agree byte for byte across the corpus, fuzz-verified to zero divergences; the language table, docs, editors, specs, completions and the OCaml reference follow, and 29 built-in languages become 30. --- .pre-commit-hooks.yaml | 4 +- CHANGELOG.md | 4 +- README.md | 2 +- docs/commands.md | 20 + docs/comparison.md | 2 +- docs/editors.md | 6 +- docs/languages.md | 17 +- docs/ocomment.1 | 2 + editors/vscode/CHANGELOG.md | 2 +- editors/vscode/README.md | 2 +- editors/vscode/package.json | 6 +- ocaml/lib/ocomment_ref.ml | 233 +++++++++++- ocaml/lib/ocomment_ref.mli | 2 +- ocaml/test/test_core.ml | 9 + release-extras/_ocomment | 60 ++- release-extras/ocomment.1 | 2 + release-extras/ocomment.bash | 40 +- release-extras/ocomment.fish | 60 ++- rust/ocomment-core/src/detect.rs | 13 +- rust/ocomment-core/src/scanner.rs | 481 ++++++++++++++++++++++-- rust/ocomment-core/src/types.rs | 10 +- rust/ocomment-core/tests/languages.rs | 108 +++++- rust/ocomment-core/tests/names.rs | 3 +- rust/ocomment/assets/config.schema.json | 2 +- rust/ocomment/assets/languages.toml | 7 + rust/ocomment/src/values.rs | 1 + rust/ocomment/tests/cli.rs | 39 ++ spec/config.schema.json | 2 +- spec/fixtures/v1/builtins.json | 24 ++ spec/fixtures/v1/floor.txt | 4 +- spec/fixtures/v1/hazards.json | 27 ++ spec/languages.toml | 7 + spec/result.schema.json | 2 +- tools/fuzz_differential.py | 7 + tools/gen_docs.py | 11 + 35 files changed, 1089 insertions(+), 132 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 892924f..abf2176 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -11,7 +11,7 @@ entry: ocomment check language: system types: [text] - files: '(?i)\.(bash|c|cc|cjs|cpp|cs|css|csx|cts|cu|cuh|cxx|dart|gemspec|go|h|hh|hpp|htm|html|hxx|java|jbuilder|js|json5|jsonc|jsx|kt|kts|lua|m|markdown|md|mjs|ml|mli|mlt|mm|mts|php|phpt|phtml|podspec|py|pyi|pyw|r|rake|rb|rbi|rbw|rmd|rockspec|rs|ru|sass|sc|scala|scss|sh|shtml|sql|svelte|swift|thor|toml|ts|tsx|vue|xhtml|yaml|yml|zig|zon|zsh)$' + files: '(?i)\.(bash|c|cc|cjs|cpp|cs|css|csx|cts|cu|cuh|cxx|dart|gemspec|go|h|hh|hpp|htm|html|hxx|java|jbuilder|js|json5|jsonc|jsx|kt|kts|lua|m|markdown|md|mjs|ml|mli|mlt|mm|mts|php|phpt|phtml|pl|pm|podspec|py|pyi|pyw|r|rake|rb|rbi|rbw|rmd|rockspec|rs|ru|sass|sc|scala|scss|sh|shtml|sql|svelte|swift|t|thor|toml|ts|tsx|vue|xhtml|yaml|yml|zig|zon|zsh)$' - id: ocomment-fix name: ocomment fix description: 'Remove comments from the staged source files in place; exit 1 from the following ocomment-check run, or a file pre-commit sees modified, blocks the commit until the result is reviewed and staged.' @@ -19,4 +19,4 @@ language: system types: [text] require_serial: true - files: '(?i)\.(bash|c|cc|cjs|cpp|cs|css|csx|cts|cu|cuh|cxx|dart|gemspec|go|h|hh|hpp|htm|html|hxx|java|jbuilder|js|json5|jsonc|jsx|kt|kts|lua|m|markdown|md|mjs|ml|mli|mlt|mm|mts|php|phpt|phtml|podspec|py|pyi|pyw|r|rake|rb|rbi|rbw|rmd|rockspec|rs|ru|sass|sc|scala|scss|sh|shtml|sql|svelte|swift|thor|toml|ts|tsx|vue|xhtml|yaml|yml|zig|zon|zsh)$' + files: '(?i)\.(bash|c|cc|cjs|cpp|cs|css|csx|cts|cu|cuh|cxx|dart|gemspec|go|h|hh|hpp|htm|html|hxx|java|jbuilder|js|json5|jsonc|jsx|kt|kts|lua|m|markdown|md|mjs|ml|mli|mlt|mm|mts|php|phpt|phtml|pl|pm|podspec|py|pyi|pyw|r|rake|rb|rbi|rbw|rmd|rockspec|rs|ru|sass|sc|scala|scss|sh|shtml|sql|svelte|swift|t|thor|toml|ts|tsx|vue|xhtml|yaml|yml|zig|zon|zsh)$' diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eacbc0..c954914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to OComment will be documented here. The project follows ### Added -- Byte-oriented scanners and transformations for 29 built-in languages and the +- Byte-oriented scanners and transformations for 30 built-in languages and the documented dialects. - CLI, staged Git fixes, LSP 3.18 server, declarative profiles, and sandboxed WASM component plugins. @@ -67,7 +67,7 @@ All notable changes to OComment will be documented here. The project follows for the OCaml reference. `CONTRIBUTING.md` documents the tags. - An official VS Code extension, `P4suta.ocomment`, under `editors/vscode`. It is a client only: it launches the separately installed `ocomment lsp`, - attaches it to the thirty-four language identifiers OComment scans, and + attaches it to the thirty-five language identifiers OComment scans, and exposes the server's quick fixes, `source.fixAll.ocomment`, code lens, and pull diagnostics, plus `OComment: Remove comments in file`, `... in workspace`, `OComment: Restart server`, `OComment: Show output`, and a status diff --git a/README.md b/README.md index 162736a..eed9536 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ classification, diagnostics, edits, transformed bytes, and source maps. OComment supports Rust, OCaml, C, C++, Go, Java, JavaScript, TypeScript, Python, Shell, HTML, CSS, JSONC, SQL, Kotlin, TOML, Lua, YAML, PHP, Ruby, Zig, R, Dart, -Swift, C#, Scala, Vue, Svelte, and Markdown. +Swift, C#, Scala, Vue, Svelte, Markdown, and Perl. JSX/TSX, Objective-C/C++, GNU C/C++, CUDA, POSIX sh, Bash 5.3, zsh, PostgreSQL, MySQL, SQLite, T-SQL, and diff --git a/docs/commands.md b/docs/commands.md index 739413a..69139de 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -131,6 +131,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -328,6 +329,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -510,6 +512,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -684,6 +687,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -858,6 +862,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -1022,6 +1027,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -1186,6 +1192,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -1366,6 +1373,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -1537,6 +1545,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -1701,6 +1710,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -1874,6 +1884,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -2051,6 +2062,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -2219,6 +2231,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -2383,6 +2396,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -2551,6 +2565,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -2719,6 +2734,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -2887,6 +2903,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -3057,6 +3074,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -3221,6 +3239,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language @@ -3385,6 +3404,7 @@ Policy: - vue: Vue single-file components, whose templates are HTML with `{{ ... }}` code - svelte: Svelte components, whose templates are HTML with `{ ... }` code - markdown: Markdown documents, whose fenced code blocks are scanned as their named languages + - perl: Perl scripts and modules, whose quote words and regexes hide a `#` --dialect Force this dialect of the selected language diff --git a/docs/comparison.md b/docs/comparison.md index d478887..2af317f 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -15,7 +15,7 @@ job you have. | --- | --- | --- | --- | --- | --- | | What it is | A comment checker and remover | A Node.js library and CLI for stripping comments | A Node.js library for stripping comments | A line counter, with a comment-stripping side output | A C preprocessor | | Built to | Report, gate, and remove comments under a policy | Strip comments from JavaScript-style source | Strip comments while preserving string literals | Count lines of code | Preprocess C-family translation units | -| Language coverage | [29 languages and 17 dialects](languages.md), plus declarative profiles and WebAssembly plugins | JavaScript and other C-style syntaxes | JavaScript, JSON, CSS, HTML | Very broad, from its own per-language comment table | C, C++, Objective-C, and their preprocessed inputs | +| Language coverage | [30 languages and 17 dialects](languages.md), plus declarative profiles and WebAssembly plugins | JavaScript and other C-style syntaxes | JavaScript, JSON, CSS, HTML | Very broad, from its own per-language comment table | C, C++, Objective-C, and their preprocessed inputs | | Keeps tool directives by default | Yes — shebangs, encoding preambles, `//go:build`, lint controls, optimiser hints, MySQL versioned comments | Documents an option for keeping `/*!` "protected" comments | Documents an option for keeping `/*!` "protected" comments | Not a stated goal | Not a stated goal | | Configurable per path | Yes, `[[overrides]]` globs in `.ocomment.toml` | Through the calling program | Through the calling program | No | No | | Check-only mode with a CI exit code | Yes, `ocomment check` | No | No | No | No | diff --git a/docs/editors.md b/docs/editors.md index 9f1e0cc..fa2523c 100644 --- a/docs/editors.md +++ b/docs/editors.md @@ -21,7 +21,7 @@ vim.lsp.config.ocomment = { "rust", "ocaml", "c", "cpp", "go", "java", "javascript", "typescript", "python", "sh", "html", "css", "jsonc", "sql", "kotlin", "toml", "lua", "yaml", "php", "ruby", "zig", "r", "dart", "swift", "cs", - "scala", "vue", "svelte", "markdown", + "scala", "vue", "svelte", "markdown", "pl", }, root_markers = { ".ocomment.toml", ".git" }, } @@ -69,12 +69,12 @@ Install **OComment** from the Marketplace, or from Open VSX. The extension is a client only: it launches the `ocomment` binary, which has to be installed separately and on `PATH`, or named by `ocomment.path`. -It attaches to thirty-four language identifiers — `rust`, `ocaml`, `c`, +It attaches to thirty-five language identifiers — `rust`, `ocaml`, `c`, `cpp`, `objective-c`, `objective-cpp`, `cuda-cpp`, `go`, `java`, `javascript`, `javascriptreact`, `typescript`, `typescriptreact`, `python`, `shellscript`, `html`, `css`, `jsonc`, `sql`, `kotlin`, `toml`, `lua`, `yaml`, `php`, `ruby`, `zig`, `r`, `dart`, `swift`, `csharp`, `scala`, `vue`, `svelte`, -and `markdown` — and contributes +`markdown`, and `perl` — and contributes `OComment: Remove comments in file`, `... in workspace`, `OComment: Restart server`, and `OComment: Show output`, plus a status bar count of the removable comments in the open files. diff --git a/docs/languages.md b/docs/languages.md index 8d6b794..d47818a 100644 --- a/docs/languages.md +++ b/docs/languages.md @@ -23,8 +23,8 @@ $ ocomment strip --language rust --dialect mysql ocomment: unsupported dialect `mysql` for rust; supported: standard ``` -OComment has 29 built-in languages covering -75 file extensions and 17 named dialects. +OComment has 30 built-in languages covering +78 file extensions and 17 named dialects. | Language | Extensions | Dialects | | --- | --- | --- | @@ -57,6 +57,7 @@ OComment has 29 built-in languages covering | `vue` | `.vue` | `standard` | | `svelte` | `.svelte` | `standard` | | `markdown` | `.md`, `.markdown`, `.rmd` | `standard` | +| `perl` | `.pl`, `.pm`, `.t` | `standard` | ## Detected without an extension @@ -81,6 +82,7 @@ interpreter name appears anywhere on the line. | `swift` | — | `swift` | | `csharp` | — | `dotnet-script` | | `scala` | — | `scala-cli`, `scala` | +| `perl` | — | `perl` | ## Anything else @@ -311,6 +313,17 @@ names — ```rust, `{r}` and `c++` all reach their scanners — and an inline code span or an indented code block is opaque, so a `//` or a `/*` inside one is code text, not a comment. +Perl is scanned conservatively: a `#` runs to the end of its +line, a POD block is opaque, and every quote word — the single +and double quotes and backticks, `q`, `qq`, `qw`, `qx`, `m`, +`s`, `tr` and `y` with delimiters of their own, and the +here-documents — hides a `#` written inside it. A `/` directly +after a closing parenthesis, bracket or brace is reported as +lexically ambiguous: perl reads `f() /a#b/` as a regular +expression and `(2) / 2` as a division, and only the parse +context tells which, so the file is called invalid and nothing +is edited. + YAML is scanned lexically, and `valid` is a lexical answer: the shapes a YAML *parser* rejects are not all shapes a lexer can see. A comment line inside a multi-line plain scalar makes the file a diff --git a/docs/ocomment.1 b/docs/ocomment.1 index cec2b0a..a0de506 100644 --- a/docs/ocomment.1 +++ b/docs/ocomment.1 @@ -117,6 +117,8 @@ vue: Vue single\-file components, whose templates are HTML with `{{ ... }}` code svelte: Svelte components, whose templates are HTML with `{ ... }` code .IP \(bu 2 markdown: Markdown documents, whose fenced code blocks are scanned as their named languages +.IP \(bu 2 +perl: Perl scripts and modules, whose quote words and regexes hide a `#` .RE .TP \fB\-\-dialect\fR \fI\fR diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md index 091d559..d9d08bf 100644 --- a/editors/vscode/CHANGELOG.md +++ b/editors/vscode/CHANGELOG.md @@ -8,7 +8,7 @@ follow the repository's tags. ### Added -- First release. Launches `ocomment lsp` and attaches it to the thirty-four +- First release. Launches `ocomment lsp` and attaches it to the thirty-five language identifiers OComment scans, including `objective-c`, `objective-cpp`, `cuda-cpp`, `javascriptreact`, `typescriptreact`, and `shellscript`. diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 7bec81c..365f61f 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -42,7 +42,7 @@ it opens the output channel. | `ocomment.enable` | `true` | Run the server at all. | | `ocomment.path` | `""` | The `ocomment` executable. Empty means the first one on `PATH`; a relative path is resolved against the workspace, and a leading `~` is expanded. | | `ocomment.extraArgs` | `[]` | Extra arguments after `lsp`, such as `["--config", "tools/ocomment.toml"]`. | -| `ocomment.languages` | the 34 identifiers above | Which language identifiers the server is attached to. | +| `ocomment.languages` | the 35 identifiers above | Which language identifiers the server is attached to. | | `ocomment.trace.server` | `"off"` | Log the traffic to the output channel. | Everything else — which comments count as removable, which are protected, per diff --git a/editors/vscode/package.json b/editors/vscode/package.json index a2bfe8d..124f8b0 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -1,7 +1,7 @@ { "name": "ocomment", "displayName": "OComment", - "description": "Fast, byte-preserving comment checker and remover for 29 languages.", + "description": "Fast, byte-preserving comment checker and remover for 30 languages.", "version": "0.1.0", "publisher": "P4suta", "license": "MIT", @@ -73,6 +73,7 @@ "onLanguage:vue", "onLanguage:svelte", "onLanguage:markdown", + "onLanguage:perl", "workspaceContains:**/.ocomment.toml" ], "contributes": { @@ -139,7 +140,8 @@ "scala", "vue", "svelte", - "markdown" + "markdown", + "perl" ], "description": "Language identifiers the server is attached to.", "scope": "window" diff --git a/ocaml/lib/ocomment_ref.ml b/ocaml/lib/ocomment_ref.ml index 935aa52..b910df2 100644 --- a/ocaml/lib/ocomment_ref.ml +++ b/ocaml/lib/ocomment_ref.ml @@ -1,7 +1,7 @@ type language = | Rust | Ocaml | C | Cpp | Go | Java | JavaScript | TypeScript | Python | Shell | Html | Css | Jsonc | Sql | Kotlin | Toml | Lua | Yaml | Php | Ruby - | Zig | R | Dart | Swift | CSharp | Scala | Vue | Svelte | Markdown | Unknown + | Zig | R | Dart | Swift | CSharp | Scala | Vue | Svelte | Markdown | Perl | Unknown type dialect = | Standard | Jsx | Tsx | ObjectiveC | ObjectiveCpp | GnuC | GnuCpp | Cuda @@ -95,6 +95,7 @@ let language_of_string value = | "vue" -> Ok Vue | "svelte" -> Ok Svelte | "markdown" -> Ok Markdown + | "perl" -> Ok Perl | other -> Error ("unsupported language `" ^ other ^ "`") let string_of_language = function @@ -109,6 +110,7 @@ let string_of_language = function | Vue -> "vue" | Svelte -> "svelte" | Markdown -> "markdown" + | Perl -> "perl" | Unknown -> "unknown" let string_of_comment_kind = function @@ -5206,6 +5208,233 @@ let vue_style_language lang = indented code block is opaque: a "//" or a "/*" in one is code text, not a comment. Every construct is recognised at its own start and read forward, so no decision depends on a byte behind a restart. *) +(* NOTE: One Perl document. Perl's lexical surface is almost all quote + words: the single and double quotes and backticks, the "q", "qq", "qw" and + "qx" forms, the "m", "s", "tr" and "y" operators with delimiters of their + own, and the here-documents, all hide a "#" written inside them, and a POD + block is opaque. The one place the bytes do not settle the reading is a + "/" directly after a closing parenthesis, bracket or brace: perl reads + "f() /a#b/" as a regular expression and "(2) / 2" as a division, and only + the parse context tells which, so this scanner reports that "/" as + lexically ambiguous and refuses to edit the file, keeping the "#" that + would decide the other way out of reach. *) +let scan_perl source language options accumulator = + let length = Bytes.length source in + let word_byte byte = + (byte >= 'a' && byte <= 'z') || (byte >= 'A' && byte <= 'Z') + || (byte >= '0' && byte <= '9') || byte = '_' in + let word_allows_regex word = + match word with + | "return" | "if" | "unless" | "while" | "until" | "for" | "foreach" + | "and" | "or" | "not" | "print" | "printf" | "say" | "split" | "grep" + | "map" | "join" | "sort" | "push" | "unshift" | "pop" | "shift" + | "splice" | "index" | "length" | "substr" | "chomp" | "chop" | "lc" + | "uc" -> true + | _ -> false in + let section_end start delimiter = + let close = match delimiter with + | '(' -> ')' | '[' -> ']' | '{' -> '}' | '<' -> '>' + | other -> other in + if delimiter = close then begin + let rec loop index = + if index >= length then None + else if Bytes.get source index = '\\' then loop (min length (index + 2)) + else if Bytes.get source index = delimiter then Some (index + 1) + else loop (index + 1) in + loop (start + 1) + end else begin + let rec loop index depth = + if index >= length then None + else if Bytes.get source index = '\\' then loop (min length (index + 2)) depth + else if Bytes.get source index = delimiter then loop (index + 1) (depth + 1) + else if Bytes.get source index = close then + if depth = 1 then Some (index + 1) else loop (index + 1) (depth - 1) + else loop (index + 1) depth in + loop (start + 1) 1 + end in + let pod start = + let rec loop index = + if index >= length then length + else begin + let line_finish = line_end source index in + if index = start then begin + if line_finish >= length then length + else loop (consume_newline source line_finish) + end else if starts source index "=cut" then + if line_finish >= length then line_finish + else consume_newline source line_finish + else if line_finish >= length then length + else loop (consume_newline source line_finish) + end in + loop start in + let quoted start = + let quote = Bytes.get source start in + let rec loop index backslashes = + if index >= length then length + else if Bytes.get source index = '\\' then loop (index + 1) (backslashes + 1) + else if Bytes.get source index = quote then + if quote = '\'' then + if backslashes mod 2 = 0 then index + 1 else loop (index + 1) 0 + else index + 1 + else loop (index + 1) 0 in + loop (start + 1) 0 in + let quote_word start = + let letter = Bytes.get source start in + let rec skip_space index = + if index < length && Bytes.get source index = ' ' then skip_space (index + 1) else index in + let second_letter index = + if index < length && word_byte (Bytes.get source index) then + if (letter = 'q' && (Bytes.get source index = 'q' || Bytes.get source index = 'w' + || Bytes.get source index = 'x' || Bytes.get source index = 'r')) + || (letter = 't' && Bytes.get source index = 'r') + then (index + 1, true) else (index, false) + else (index, false) in + let cursor, _doubled = second_letter (start + 1) in + let cursor = skip_space cursor in + if cursor >= length then None + else if word_byte (Bytes.get source cursor) then None + else match section_end cursor (Bytes.get source cursor) with + | None -> None + | Some first -> + if letter = 's' || letter = 't' || letter = 'y' then + if first >= length then None + else if word_byte (Bytes.get source first) then None + else Option.map (fun second_end -> second_end) (section_end first (Bytes.get source first)) + else Some first in + let heredoc start = + let rec terminator_word index acc = + if index < length && word_byte (Bytes.get source index) + then terminator_word (index + 1) (acc ^ String.make 1 (Bytes.get source index)) + else (index, acc) in + let cursor = start + 2 in + let indented = cursor < length && Bytes.get source cursor = '~' in + let cursor = if indented then cursor + 1 else cursor in + let cursor, terminator = + if cursor < length && (Bytes.get source cursor = '\'' || Bytes.get source cursor = '"' + || Bytes.get source cursor = '`') then begin + let quote = Bytes.get source cursor in + let rec inner index acc = + if index >= length then (length, acc) + else if Bytes.get source index = quote then (index + 1, acc) + else inner (index + 1) (acc ^ String.make 1 (Bytes.get source index)) in + inner (cursor + 1) "" + end else terminator_word cursor "" in + if terminator = "" then None + else begin + let line_finish = line_end source cursor in + let rec body index = + if index >= length then Some length + else begin + let line_finish = line_end source index in + let rec content cursor = + if indented && cursor < line_finish && (Bytes.get source cursor = ' ' + || Bytes.get source cursor = '\t') + then content (cursor + 1) else cursor in + let content = content index in + if Bytes.sub_string source content (line_finish - content) = terminator then + Some (if line_finish >= length then line_finish else consume_newline source line_finish) + else if line_finish >= length then Some length + else body (consume_newline source line_finish) + end in + if line_finish >= length then Some length else body (consume_newline source line_finish) + end in + let regex start = + let rec loop index = + if index >= length then length + else if Bytes.get source index = '\\' then loop (min length (index + 2)) + else if Bytes.get source index = '[' then begin + let rec class_index index = + if index >= length then length + else if Bytes.get source index = ']' then index + 1 + else if Bytes.get source index = '\\' then class_index (min length (index + 2)) + else class_index (index + 1) in + loop (class_index (index + 1)) + end + else if Bytes.get source index = '/' then begin + let rec modifiers index = + if index < length && word_byte (Bytes.get source index) then modifiers (index + 1) else index in + modifiers (index + 1) + end + else loop (index + 1) in + loop (start + 1) in + let rec loop index regex_allowed = + if index >= length then () + else if (index = 0 || Bytes.get source (index - 1) = '\n' || Bytes.get source (index - 1) = '\r') + && Bytes.get source index = '=' + && index + 1 < length && word_byte (Bytes.get source (index + 1)) + then loop (pod index) regex_allowed + else if Bytes.get source index = '#' then begin + let finish = line_end source (index + 1) in + add_comment accumulator source language options Line index finish; + loop finish (Some true) + end + else if Bytes.get source index = '\'' || Bytes.get source index = '"' || Bytes.get source index = '`' + then loop (quoted index) (Some false) + else if Bytes.get source index = '<' && index + 1 < length && Bytes.get source (index + 1) = '<' + then (match heredoc index with + | Some finish -> loop finish (Some false) + | None -> loop (index + 1) regex_allowed) + else if (index = 0 || not (word_byte (Bytes.get source (index - 1)))) + && (Bytes.get source index = 'q' || Bytes.get source index = 'm' + || Bytes.get source index = 's' || Bytes.get source index = 't' + || Bytes.get source index = 'y') + then (match quote_word index with + | Some finish -> loop finish (Some false) + | None -> + (* NOTE: the letter is a bareword, so the `/` after it is settled by + the same rules as after any other word. *) + let start = index in + let rec ident index = + if index < length && word_byte (Bytes.get source index) then ident (index + 1) else index in + let finish = ident index in + loop finish (Some (word_allows_regex (Bytes.sub_string source start (finish - start))))) + else if Bytes.get source index = '/' then begin + match regex_allowed with + | Some true -> + let finish = regex index in + loop finish (Some false) + | Some false -> loop (index + 1) (Some true) + | None -> + let finish = regex index in + add_error accumulator "lexical-ambiguity" + "ambiguous `/` after a closing delimiter: a regex or a division" index finish; + loop finish (Some false) + end + else if Bytes.get source index = ')' || Bytes.get source index = ']' || Bytes.get source index = '}' + then loop (index + 1) None + else if Bytes.get source index = '(' || Bytes.get source index = '[' || Bytes.get source index = '{' + then loop (index + 1) (Some true) + else if Bytes.get source index = '$' || Bytes.get source index = '@' || Bytes.get source index = '%' + then begin + let rec ident index = + if index < length && word_byte (Bytes.get source index) then ident (index + 1) else index in + loop (ident (index + 1)) (Some false) + end + else if (Bytes.get source index >= 'a' && Bytes.get source index <= 'z') + || (Bytes.get source index >= 'A' && Bytes.get source index <= 'Z') + || Bytes.get source index = '_' then begin + let start = index in + let rec ident index = + if index < length && word_byte (Bytes.get source index) then ident (index + 1) else index in + let finish = ident index in + loop finish (Some (word_allows_regex (Bytes.sub_string source start (finish - start)))) + end + else if Bytes.get source index >= '0' && Bytes.get source index <= '9' then begin + let rec digits index = + if index < length && word_byte (Bytes.get source index) then digits (index + 1) else index in + loop (digits index) (Some false) + end + else if Bytes.get source index = '=' || Bytes.get source index = '+' || Bytes.get source index = '-' + || Bytes.get source index = '*' || Bytes.get source index = '%' || Bytes.get source index = '!' + || Bytes.get source index = '~' || Bytes.get source index = '&' || Bytes.get source index = '|' + || Bytes.get source index = '?' || Bytes.get source index = ':' || Bytes.get source index = ',' + || Bytes.get source index = ';' || Bytes.get source index = '<' || Bytes.get source index = '>' + then loop (index + 1) (Some true) + else if Bytes.get source index = '\r' || Bytes.get source index = '\n' + then loop (consume_newline source index) (Some true) + else loop (index + 1) regex_allowed + in ignore (loop 0 (Some true)) + let scan_markdown source language options accumulator = let length = Bytes.length source in let html_comment index = @@ -5288,6 +5517,7 @@ let scan_markdown source language options accumulator = | CSharp -> scan_csharp child_source embedded options child | Scala -> scan_scala child_source embedded options child | R -> scan_r child_source embedded options child + | Perl -> scan_perl child_source embedded options child | Html | Markdown | Vue | Svelte | Unknown -> ()); List.iter (fun (comment : comment) -> accumulator.comments_rev <- { comment with span = { start = comment.span.start + info_finish; finish = comment.span.finish + info_finish } } :: accumulator.comments_rev) (List.rev child.comments_rev); @@ -5606,6 +5836,7 @@ and scan source language options = | Vue -> scan_vue source language options accumulator | Svelte -> scan_svelte source language options accumulator | Markdown -> scan_markdown source language options accumulator + | Perl -> scan_perl source language options accumulator | R -> scan_r source language options accumulator | Html -> scan_html source language options accumulator | Unknown -> add_error accumulator "unknown-language" "a language is required" 0 0); diff --git a/ocaml/lib/ocomment_ref.mli b/ocaml/lib/ocomment_ref.mli index 7db880c..9cae9f6 100644 --- a/ocaml/lib/ocomment_ref.mli +++ b/ocaml/lib/ocomment_ref.mli @@ -1,7 +1,7 @@ type language = | Rust | Ocaml | C | Cpp | Go | Java | JavaScript | TypeScript | Python | Shell | Html | Css | Jsonc | Sql | Kotlin | Toml | Lua | Yaml | Php | Ruby - | Zig | R | Dart | Swift | CSharp | Scala | Vue | Svelte | Markdown | Unknown + | Zig | R | Dart | Swift | CSharp | Scala | Vue | Svelte | Markdown | Perl | Unknown type dialect = | Standard | Jsx | Tsx | ObjectiveC | ObjectiveCpp | GnuC | GnuCpp | Cuda diff --git a/ocaml/test/test_core.ml b/ocaml/test/test_core.ml index fe61675..8103c67 100644 --- a/ocaml/test/test_core.ml +++ b/ocaml/test/test_core.ml @@ -297,6 +297,14 @@ let check_markdown_fences () = Alcotest.(check bool) "valid" true report.valid; Alcotest.(check int) "only the rust comment" 1 (List.length report.comments) +(* NOTE: Perl's POD blocks are opaque, and its division comments are + comments. *) +let check_perl_pod () = + let source = "=head1 NAME\n# not a comment\n=cut\nmy $x = 1; # comment\n" in + let report = scan (Bytes.of_string source) Perl default_scan_options in + Alcotest.(check bool) "valid" true report.valid; + Alcotest.(check int) "only the real comment" 1 (List.length report.comments) + let () = Alcotest.run "ocomment-ref" [ "core", [ Alcotest.test_case "transform" `Quick check_transform; @@ -322,5 +330,6 @@ let () = Alcotest.run "ocomment-ref" [ check_scala_xml_and_interpolation; Alcotest.test_case "vue-component" `Quick check_vue_component; Alcotest.test_case "markdown-fences" `Quick check_markdown_fences; + Alcotest.test_case "perl-pod" `Quick check_perl_pod; ] ] diff --git a/release-extras/_ocomment b/release-extras/_ocomment index 0ae6b1c..3a733ac 100644 --- a/release-extras/_ocomment +++ b/release-extras/_ocomment @@ -50,7 +50,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -153,7 +154,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -249,7 +251,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -348,7 +351,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -444,7 +448,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -540,7 +545,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -633,7 +639,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -726,7 +733,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -823,7 +831,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -917,7 +926,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -1010,7 +1020,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -1114,7 +1125,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -1208,7 +1220,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -1302,7 +1315,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -1395,7 +1409,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -1489,7 +1504,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -1583,7 +1599,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -1725,7 +1742,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -1819,7 +1837,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" @@ -1912,7 +1931,8 @@ csharp\:"C# source and script files, whose \`#\` lines are preprocessor directiv scala\:"Scala source and script files, whose block comments nest and whose XML literals are opaque" vue\:"Vue single-file components, whose templates are HTML with \`{{ ... }}\` code" svelte\:"Svelte components, whose templates are HTML with \`{ ... }\` code" -markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages"))' \ +markdown\:"Markdown documents, whose fenced code blocks are scanned as their named languages" +perl\:"Perl scripts and modules, whose quote words and regexes hide a \`#\`"))' \ '--dialect=[Force this dialect of the selected language]:DIALECT:((standard\:"The default lexical rules of the language" jsx\:"JavaScript with JSX elements" tsx\:"TypeScript with JSX elements" diff --git a/release-extras/ocomment.1 b/release-extras/ocomment.1 index cec2b0a..a0de506 100644 --- a/release-extras/ocomment.1 +++ b/release-extras/ocomment.1 @@ -117,6 +117,8 @@ vue: Vue single\-file components, whose templates are HTML with `{{ ... }}` code svelte: Svelte components, whose templates are HTML with `{ ... }` code .IP \(bu 2 markdown: Markdown documents, whose fenced code blocks are scanned as their named languages +.IP \(bu 2 +perl: Perl scripts and modules, whose quote words and regexes hide a `#` .RE .TP \fB\-\-dialect\fR \fI\fR diff --git a/release-extras/ocomment.bash b/release-extras/ocomment.bash index 2dfa6c7..58d313d 100644 --- a/release-extras/ocomment.bash +++ b/release-extras/ocomment.bash @@ -186,7 +186,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -244,7 +244,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -302,7 +302,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -360,7 +360,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -418,7 +418,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -476,7 +476,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -534,7 +534,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -886,7 +886,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -944,7 +944,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -1002,7 +1002,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -1060,7 +1060,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -1118,7 +1118,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -1188,7 +1188,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -1358,7 +1358,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -1416,7 +1416,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -1474,7 +1474,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -1532,7 +1532,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -1590,7 +1590,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -1648,7 +1648,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) @@ -1706,7 +1706,7 @@ _ocomment() { return 0 ;; --language) - COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown" -- "${cur}")) + COMPREPLY=($(compgen -W "rust ocaml c cpp go java javascript typescript python shell html css jsonc sql kotlin toml lua yaml php ruby zig r dart swift csharp scala vue svelte markdown perl" -- "${cur}")) return 0 ;; --dialect) diff --git a/release-extras/ocomment.fish b/release-extras/ocomment.fish index 4b76177..2935680 100644 --- a/release-extras/ocomment.fish +++ b/release-extras/ocomment.fish @@ -59,7 +59,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_needs_command" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -170,7 +171,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand check" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -268,7 +270,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand fix" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -368,7 +371,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand diff" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -466,7 +470,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand scan" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -564,7 +569,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand strip" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -660,7 +666,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand lsp" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -756,7 +763,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand init" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -855,7 +863,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand config" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -951,7 +960,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand languages" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -1047,7 +1057,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and not __fish_seen_subcommand_from add remove list update verify new help" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -1153,7 +1164,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from add" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -1249,7 +1261,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from remove" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -1345,7 +1358,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from list" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -1441,7 +1455,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from update" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -1537,7 +1552,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from verify" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -1633,7 +1649,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand plugin; and __fish_seen_subcommand_from new" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -1736,7 +1753,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand completions" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -1832,7 +1850,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand doctor" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' @@ -1928,7 +1947,8 @@ csharp\t'C# source and script files, whose `#` lines are preprocessor directives scala\t'Scala source and script files, whose block comments nest and whose XML literals are opaque' vue\t'Vue single-file components, whose templates are HTML with `{{ ... }}` code' svelte\t'Svelte components, whose templates are HTML with `{ ... }` code' -markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages'" +markdown\t'Markdown documents, whose fenced code blocks are scanned as their named languages' +perl\t'Perl scripts and modules, whose quote words and regexes hide a `#`'" complete -c ocomment -n "__fish_ocomment_using_subcommand man" -l dialect -d 'Force this dialect of the selected language' -r -f -a "standard\t'The default lexical rules of the language' jsx\t'JavaScript with JSX elements' tsx\t'TypeScript with JSX elements' diff --git a/rust/ocomment-core/src/detect.rs b/rust/ocomment-core/src/detect.rs index 3ae30c7..d0e05f0 100644 --- a/rust/ocomment-core/src/detect.rs +++ b/rust/ocomment-core/src/detect.rs @@ -62,7 +62,7 @@ enum Spelling { /// searching for it that way would read every `#!/usr/bin/awk` on disk as R. /// It is a [`Spelling::Word`] instead, and it is listed last so that every name /// spelled out in full is met before a bare letter is considered at all. -const SHEBANGS: [(&str, Language, Dialect, Spelling); 19] = [ +const SHEBANGS: [(&str, Language, Dialect, Spelling); 20] = [ ( "python", Language::Python, @@ -121,6 +121,12 @@ const SHEBANGS: [(&str, Language, Dialect, Spelling); 19] = [ Dialect::Standard, Spelling::Anywhere, ), + ( + "perl", + Language::Perl, + Dialect::Standard, + Spelling::Anywhere, + ), ( "scala-cli", Language::Scala, @@ -324,6 +330,11 @@ pub fn detect_language(path: Option<&Path>, source: &[u8]) -> Option * the fenced-block scan — which is what the note that once kept * it from the R entry is now the record of. */ "md" | "markdown" | "rmd" => Some((Language::Markdown, Dialect::Standard)), + /* NOTE: `.pl`, `.pm` and `.t` are Perl — a program, a module and + * a test — and so is a `perl` `#!` line. `.pod` is deliberately + * absent: a POD document is documentation only, with no code to + * scan. */ + "pl" | "pm" | "t" => Some((Language::Perl, Dialect::Standard)), "vue" => Some((Language::Vue, Dialect::Standard)), "svelte" => Some((Language::Svelte, Dialect::Standard)), "scss" | "sass" => Some((Language::Css, Dialect::Scss)), diff --git a/rust/ocomment-core/src/scanner.rs b/rust/ocomment-core/src/scanner.rs index bad4c9b..d54dab7 100644 --- a/rust/ocomment-core/src/scanner.rs +++ b/rust/ocomment-core/src/scanner.rs @@ -304,6 +304,7 @@ impl<'a> Scanner<'a> { Language::Vue => self.scan_vue(), Language::Svelte => self.scan_svelte(), Language::Markdown => self.scan_markdown(), + Language::Perl => self.scan_perl(), Language::Unknown => self.error( "unknown-language", "a language is required", @@ -4903,6 +4904,330 @@ impl<'a> Scanner<'a> { self.scan_sfc(false); } + /// One Perl document. + /// + /// Perl's lexical surface is almost all quote words: the single and + /// double quotes and backticks, the `q`, `qq`, `qw` and `qx` forms, the + /// `m`, `s`, `tr` and `y` operators with delimiters of their own, and the + /// here-documents, all hide a `#` written inside them, and a POD block is + /// opaque. The one place the bytes do not settle the reading is a `/` + /// directly after a closing parenthesis, bracket or brace: perl reads + /// `f() /a#b/` as a regular expression and `(2) / 2` as a division, and + /// only the parse context tells which, so this scanner reports that `/` + /// as lexically ambiguous and refuses to edit the file, keeping the `#` + /// that would decide the other way out of reach. + fn scan_perl(&mut self) { + let bytes = self.source; + let mut index = 0; + let mut regex_allowed: Option = Some(true); + while index < bytes.len() && !self.stopped { + if (index == 0 || matches!(bytes[index - 1], b'\n' | b'\r')) + && bytes[index] == b'=' + && bytes + .get(index + 1) + .is_some_and(|byte| byte.is_ascii_alphanumeric() || *byte == b'_') + { + index = self.scan_perl_pod(index); + continue; + } + if bytes[index] == b'#' { + let end = line_end(bytes, index + 1); + self.add_comment(index, end, CommentKind::Line); + index = end; + regex_allowed = Some(true); + continue; + } + if matches!(bytes[index], b'\'' | b'"' | b'`') { + index = self.scan_perl_quoted(index); + regex_allowed = Some(false); + continue; + } + if bytes[index] == b'<' + && bytes.get(index + 1) == Some(&b'<') + && let Some(end) = self.scan_perl_heredoc(index) + { + index = end; + regex_allowed = Some(false); + continue; + } + if (index == 0 || !is_perl_word_byte(bytes[index - 1])) + && matches!(bytes[index], b'q' | b'm' | b's' | b't' | b'y') + && let Some(end) = self.scan_perl_quote_word(index) + { + index = end; + regex_allowed = Some(false); + continue; + } + if bytes[index] == b'/' { + match regex_allowed { + Some(true) => { + let end = self.scan_perl_regex(index); + index = end; + regex_allowed = Some(false); + } + Some(false) => { + index += 1; + regex_allowed = Some(true); + } + None => { + let end = self.scan_perl_regex(index); + self.error( + "lexical-ambiguity", + "ambiguous `/` after a closing delimiter: a regex or a division", + ByteSpan::new(index, end), + ); + index = end; + regex_allowed = Some(false); + } + } + continue; + } + if bytes[index] == b')' || bytes[index] == b']' || bytes[index] == b'}' { + index += 1; + regex_allowed = None; + continue; + } + if bytes[index] == b'(' || bytes[index] == b'[' || bytes[index] == b'{' { + index += 1; + regex_allowed = Some(true); + continue; + } + if bytes[index] == b'$' || bytes[index] == b'@' || bytes[index] == b'%' { + index += 1; + while index < bytes.len() && is_perl_word_byte(bytes[index]) { + index += 1; + } + regex_allowed = Some(false); + continue; + } + if bytes[index].is_ascii_alphabetic() || bytes[index] == b'_' { + let start = index; + index += 1; + while index < bytes.len() && is_perl_word_byte(bytes[index]) { + index += 1; + } + let word = &bytes[start..index]; + regex_allowed = perl_word_allows_regex(word); + continue; + } + if bytes[index].is_ascii_digit() { + while index < bytes.len() + && (bytes[index].is_ascii_alphanumeric() || matches!(bytes[index], b'.' | b'_')) + { + index += 1; + } + regex_allowed = Some(false); + continue; + } + match bytes[index] { + b'=' | b'+' | b'-' | b'*' | b'%' | b'!' | b'~' | b'&' | b'|' | b'?' | b':' + | b',' | b';' | b'<' | b'>' => { + index += 1; + regex_allowed = Some(true); + } + b'\r' | b'\n' => { + index = consume_newline(bytes, index); + self.add_safe_checkpoint(index); + regex_allowed = Some(true); + } + _ => index += 1, + } + } + } + + /// One POD block, beginning at the `=` of its marker. The block is opaque + /// until a line that opens with `=cut`; a stray `=cut` with nothing open + /// is a line like any other. + fn scan_perl_pod(&mut self, start: usize) -> usize { + let bytes = self.source; + let mut index = start; + while index < bytes.len() { + let line_finish = line_end(bytes, index); + if (index == start || bytes.get(index).copied() == Some(b'=')) + && starts(bytes, index, b"=cut") + && index != start + { + return if line_finish >= bytes.len() { + line_finish + } else { + consume_newline(bytes, line_finish) + }; + } + if index == start { + let pod_line = line_end(bytes, start); + if pod_line >= bytes.len() { + return bytes.len(); + } + index = consume_newline(bytes, pod_line); + continue; + } + if line_finish >= bytes.len() { + return bytes.len(); + } + index = consume_newline(bytes, line_finish); + } + bytes.len() + } + + /// One Perl string or command substitution, beginning at its quote. + fn scan_perl_quoted(&mut self, start: usize) -> usize { + let bytes = self.source; + let quote = bytes[start]; + let mut index = start + 1; + let mut backslashes = 0usize; + while index < bytes.len() { + if bytes[index] == b'\\' { + backslashes += 1; + index += 1; + continue; + } + if bytes[index] == quote { + if quote == b'\'' { + if backslashes.is_multiple_of(2) { + return index + 1; + } + } else { + return index + 1; + } + } + backslashes = 0; + if quote != b'\'' && bytes[index] == b'\\' { + index = (index + 1).min(bytes.len()); + } + index += 1; + } + bytes.len() + } + + /// One Perl quote word, here-document or the `m`, `s`, `tr` and `y` + /// operators, beginning at the letter that names it, or `None` when the + /// letter is a bareword rather than a quote. + fn scan_perl_quote_word(&mut self, start: usize) -> Option { + let bytes = self.source; + let mut cursor = start; + let mut form = Vec::new(); + form.push(bytes[cursor]); + if matches!(bytes[cursor], b'q' | b't') + && let Some(&second) = bytes.get(cursor + 1) + && ((bytes[cursor] == b'q' && matches!(second, b'q' | b'w' | b'x' | b'r')) + || (bytes[cursor] == b't' && second == b'r')) + { + form.push(second); + cursor += 1; + } + cursor += 1; + while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() && bytes[cursor] != b'\n' + { + cursor += 1; + } + let delimiter = bytes.get(cursor).copied()?; + if is_perl_word_byte(delimiter) { + return None; + } + let first = perl_section_end(bytes, cursor, delimiter)?; + if matches!(form[0], b's' | b't' | b'y') { + let second = bytes.get(first).copied()?; + if is_perl_word_byte(second) { + return None; + } + let second_end = perl_section_end(bytes, first, second)?; + return Some(second_end); + } + Some(first) + } + + /// One Perl here-document, beginning at its `<<`, or `None` when the + /// `<<` is the shift operator. + fn scan_perl_heredoc(&mut self, start: usize) -> Option { + let bytes = self.source; + let mut cursor = start + 2; + let indented = bytes.get(cursor) == Some(&b'~'); + if indented { + cursor += 1; + } + let mut terminator = Vec::new(); + if matches!(bytes.get(cursor), Some(b'\'' | b'"' | b'`')) { + let quote = bytes[cursor]; + cursor += 1; + while cursor < bytes.len() && bytes[cursor] != quote { + terminator.push(bytes[cursor]); + cursor += 1; + } + cursor += 1; + } else { + while cursor < bytes.len() && is_perl_word_byte(bytes[cursor]) { + terminator.push(bytes[cursor]); + cursor += 1; + } + } + if terminator.is_empty() { + return None; + } + let line_finish = line_end(bytes, cursor); + let mut body = if line_finish >= bytes.len() { + return Some(bytes.len()); + } else { + consume_newline(bytes, line_finish) + }; + while body < bytes.len() { + let line_finish = line_end(bytes, body); + let mut content = body; + if indented { + while content < line_finish && matches!(bytes[content], b' ' | b'\t') { + content += 1; + } + } + if bytes[content..line_finish] == terminator[..] { + return Some(if line_finish >= bytes.len() { + line_finish + } else { + consume_newline(bytes, line_finish) + }); + } + body = if line_finish >= bytes.len() { + bytes.len() + } else { + consume_newline(bytes, line_finish) + }; + } + Some(bytes.len()) + } + + /// One Perl regular expression, beginning at its `/`, to the unescaped + /// `/` that closes it — a `/` inside a character class is content — and + /// past the modifiers that follow. + fn scan_perl_regex(&mut self, start: usize) -> usize { + let bytes = self.source; + let mut index = start + 1; + while index < bytes.len() { + if bytes[index] == b'\\' { + index = (index + 2).min(bytes.len()); + continue; + } + if bytes[index] == b'[' { + index += 1; + while index < bytes.len() && bytes[index] != b']' { + if bytes[index] == b'\\' { + index = (index + 2).min(bytes.len()); + } else { + index += 1; + } + } + index += 1; + continue; + } + if bytes[index] == b'/' { + index += 1; + while index < bytes.len() && is_perl_word_byte(bytes[index]) { + index += 1; + } + return index; + } + index += 1; + } + bytes.len() + } + /// One Markdown document. /// /// An HTML comment is a comment, a fenced code block is scanned as the @@ -5497,13 +5822,13 @@ fn java_text_block_end(source: &[u8], start: usize) -> (usize, bool) { let mut index = start.saturating_add(3); while index + 2 < source.len() { if starts(source, index, b"\"\"\"") { - let mut backslashes = 0; + let mut backslashes = 0usize; let mut cursor = index; while cursor > start + 3 && source[cursor - 1] == b'\\' { backslashes += 1; cursor -= 1; } - if backslashes % 2 == 0 { + if backslashes.is_multiple_of(2) { return (index + 3, true); } } @@ -5679,46 +6004,33 @@ impl RestartRules { /// would read the rest of the tag as ordinary text and find a comment in it /// the full scan protected. /// -/// The test is the quote structure. A position with a quote of a kind on each -/// side of it — each the nearest of its kind, so no quote of the kind lies -/// between them — sits inside the pair they form: that is how a quoted -/// attribute is recognised, and it is asked of the offset itself and of every -/// byte the walk steps over, so the walk crosses only unquoted `<` and `>`. -/// A suffix scan — whose own source begins at the checkpoint — gives the -/// same answers, because a checkpoint that is itself sound, as every recorded -/// one is, sits inside no pair and no tag, so the pairs and tags its walk -/// meets lie within its bytes. +/// The test walks back to the nearest `<` and reads the tag forward from it, +/// exactly as the scan would: the quotes pair and the first unquoted `>` +/// ends the tag. The offset is refused when the tag is still open there — its +/// `>` lies beyond it or never comes. A suffix scan — whose own source begins +/// at the checkpoint — gives the same answer, because a checkpoint that is +/// itself sound, as every recorded one is, sits inside no tag, so the `<` +/// its walk meets lies within its bytes. fn the_tag_boundary_permits_a_restart(source: &[u8], offset: usize) -> bool { - for kind in b"\"'".iter().copied() { - if source[..offset] - .iter() - .rposition(|byte| *byte == kind) - .is_some() - && source - .get(offset + 1..) - .is_some_and(|rest| rest.contains(&kind)) - { - return false; - } - } let mut index = offset; while index > 0 { index -= 1; - let quoted = b"\"'".iter().any(|kind| { - source[..index] - .iter() - .rposition(|byte| byte == kind) - .is_some() - && source - .get(index + 1..) - .is_some_and(|rest| rest.contains(kind)) - }); - if !quoted { - match source[index] { - b'>' => return true, - b'<' => return false, - _ => {} + if source[index] == b'<' { + let mut cursor = index + 1; + let mut quote = None; + while cursor < offset { + if let Some(active) = quote { + if source[cursor] == active { + quote = None; + } + } else if matches!(source[cursor], b'\'' | b'"') { + quote = Some(source[cursor]); + } else if source[cursor] == b'>' { + return true; + } + cursor += 1; } + return false; } } true @@ -8578,6 +8890,101 @@ fn markdown_fence_language(info: &[u8]) -> Option { } } +/// A byte a Perl word may carry. +fn is_perl_word_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || matches!(byte, b'_') +} + +/// Whether the word at the front of a token leaves the next `/` a regex +/// opener: the functions and operators perl reads a term after. +fn perl_word_allows_regex(word: &[u8]) -> Option { + matches!( + word, + b"return" + | b"if" + | b"unless" + | b"while" + | b"until" + | b"for" + | b"foreach" + | b"and" + | b"or" + | b"not" + | b"print" + | b"printf" + | b"say" + | b"split" + | b"grep" + | b"map" + | b"join" + | b"sort" + | b"push" + | b"unshift" + | b"pop" + | b"shift" + | b"splice" + | b"index" + | b"length" + | b"substr" + | b"chomp" + | b"chop" + | b"lc" + | b"uc" + ) + .then_some(true) + .or(Some(false)) +} + +/// The byte after one section of a Perl quote word, beginning at its +/// delimiter: the section closes at the delimiter's mate, nesting for the +/// paired delimiters, with a `\` carrying the byte behind it in. +fn perl_section_end(bytes: &[u8], start: usize, delimiter: u8) -> Option { + let close = match delimiter { + b'(' => b')', + b'[' => b']', + b'{' => b'}', + b'<' => b'>', + other => other, + }; + if delimiter == close { + let mut index = start + 1; + while index < bytes.len() { + if bytes[index] == b'\\' { + index = (index + 2).min(bytes.len()); + continue; + } + if bytes[index] == delimiter { + return Some(index + 1); + } + index += 1; + } + return None; + } + let mut depth = 1usize; + let mut index = start + 1; + while index < bytes.len() { + if bytes[index] == b'\\' { + index = (index + 2).min(bytes.len()); + continue; + } + if bytes[index] == delimiter { + depth += 1; + index += 1; + continue; + } + if bytes[index] == close { + depth -= 1; + index += 1; + if depth == 0 { + return Some(index); + } + continue; + } + index += 1; + } + None +} + fn tag_attr_value<'a>(attrs: &'a [u8], name: &[u8]) -> Option<&'a [u8]> { let mut index = 0; while index < attrs.len() { diff --git a/rust/ocomment-core/src/types.rs b/rust/ocomment-core/src/types.rs index 72c526a..fc49119 100644 --- a/rust/ocomment-core/src/types.rs +++ b/rust/ocomment-core/src/types.rs @@ -160,6 +160,11 @@ pub enum Language { /// scanned as the language their info string names, and inline and /// indented code are opaque. Markdown, + /// Perl, detected from `.pl`, `.pm` and `.t`, and from a `perl` `#!` + /// line. Its quote words, here-documents and regular expressions hide a + /// `#`, its POD blocks are opaque, and a `/` the parse context alone + /// settles is reported as lexically ambiguous. + Perl, /// No built-in scanner, and the default. /// /// Scanning it yields no comments and one `unknown-language` error @@ -172,7 +177,7 @@ pub enum Language { impl Language { /// Every CLI-visible language; `Unknown` is deliberately excluded. - pub const ALL: [Self; 29] = [ + pub const ALL: [Self; 30] = [ Self::Rust, Self::Ocaml, Self::C, @@ -202,6 +207,7 @@ impl Language { Self::Vue, Self::Svelte, Self::Markdown, + Self::Perl, ]; /// The canonical name, identical to the serde representation. @@ -236,6 +242,7 @@ impl Language { Self::Vue => "vue", Self::Svelte => "svelte", Self::Markdown => "markdown", + Self::Perl => "perl", Self::Unknown => "unknown", } } @@ -260,6 +267,7 @@ impl Language { | Self::Vue | Self::Svelte | Self::Markdown + | Self::Perl | Self::Unknown => &[], Self::Cpp => &["c++", "cxx"], Self::Go => &["golang"], diff --git a/rust/ocomment-core/tests/languages.rs b/rust/ocomment-core/tests/languages.rs index dcdfb87..6a3ccca 100644 --- a/rust/ocomment-core/tests/languages.rs +++ b/rust/ocomment-core/tests/languages.rs @@ -4652,16 +4652,12 @@ fn r_is_detected_from_its_extension_reserved_name_and_shebang() { /* NOTE: The one-letter name is the reason the `#!` table cannot be searched * for it as a substring: `/usr/` carries an `r` and so does every second * interpreter path, so this name is compared against whole words. */ - for line in [ - b"#!/usr/bin/perl -w\n".as_slice(), - b"#!/usr/bin/awk -f\n".as_slice(), - ] { - assert!( - detect_language(None, line).is_none(), - "{:?} was read as R", - String::from_utf8_lossy(line) - ); - } + assert!( + detect_language(None, b"#!/usr/bin/awk -f\n").is_none(), + "awk was read as R" + ); + let perl = detect_language(None, b"#!/usr/bin/perl -w\n").expect("perl shebang"); + assert_eq!((perl.language, perl.reason), (Language::Perl, "shebang")); } #[test] @@ -6803,3 +6799,95 @@ fn markdown_is_detected_from_its_extensions() { assert_eq!(found.reason, "extension"); } } + +/// A Perl `#` runs to the end of its line, and a POD block is opaque: the +/// `#` inside `=head1 ... =cut` is documentation text, not a comment. +/// +/// Ground truth, perl 5.38: the source below passes `perl -c`. +#[test] +fn perl_comments_and_pod_blocks() { + let source = b"=head1 NAME\n# not a comment\n=cut\nmy $x = 1; # comment\n"; + let report = scan(source, Language::Perl, ScanOptions::default()); + assert!(report.valid, "diagnostics: {:?}", report.diagnostics); + assert_eq!(report.comments.len(), 1, "{:?}", report.comments); + assert_eq!(report.comments[0].span, ByteSpan::new(44, 53)); +} + +/// Every Perl string and quote-word form hides a `#` written inside it: the +/// single and double quotes and backticks, the `q`, `qq`, `qw` and `qx` +/// forms, the `m`, `s`, `tr` and `y` operators with their own delimiters. +/// +/// Ground truth, perl 5.38: the source below passes `perl -c`. +#[test] +fn perl_strings_and_quote_words_hide_comment_openers() { + let source = b"my $a = '# not';\nmy $b = \"# not\";\nmy $c = `# not`;\nmy $d = q{# not};\nmy $e = qq{# not};\nmy $f = qw(a # b);\nmy $g = qx{# not};\nmy $h = m{# not};\nmy $i = s{# not}{x};\nmy $j = tr{a#}{b#};\n# remove\n"; + let report = scan(source, Language::Perl, ScanOptions::default()); + assert!(report.valid, "diagnostics: {:?}", report.diagnostics); + assert_eq!(report.comments.len(), 1, "{:?}", report.comments); + assert_eq!(report.comments[0].span, ByteSpan::new(185, 193)); +} + +/// A here-document's body is opaque until the line that names the terminator, +/// however the terminator was written: plain, quoted, or with the indented +/// `<<~` form. +/// +/// Ground truth, perl 5.38: the sources below pass `perl -c`. +#[test] +fn perl_heredocs_are_opaque() { + let source = b"my $a = <<'EOF';\n# not a comment\nEOF\nmy $b = <<\"EOF\";\n# not a comment\nEOF\nmy $c = < "Svelte components, whose templates are HTML with `{ ... }` code", Language::Markdown => "Markdown documents, whose fenced code blocks are scanned as their named languages", + Language::Perl => "Perl scripts and modules, whose quote words and regexes hide a `#`", Language::Unknown => "An undetected language", }); diff --git a/rust/ocomment/tests/cli.rs b/rust/ocomment/tests/cli.rs index f29112f..f9b3c8a 100644 --- a/rust/ocomment/tests/cli.rs +++ b/rust/ocomment/tests/cli.rs @@ -6025,3 +6025,42 @@ fn a_markdown_file_scans_its_fenced_code_blocks() { b"# notes\n\n```rust\n\n```\n`// inline`\n" ); } + +/// A Perl file hides the `#` in its quote words and regexes and keeps its POD +/// opaque, while a division's `#` is a comment. +#[test] +fn a_perl_file_hides_quote_words_and_keeps_pod_opaque() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("script.pl"); + fs::write( + &path, + b"=head1 NAME\n# not a comment\n=cut\nmy $x = 'a#b';\nmy $y = $x / 2; # division\n", + ) + .unwrap(); + + let scanned = run(directory.path(), &["scan", "script.pl", "--format", "json"]); + assert_eq!( + scanned.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&scanned.stderr) + ); + let document: serde_json::Value = serde_json::from_slice(&scanned.stdout).unwrap(); + let report = &document["files"][0]["report"]; + assert_eq!(report["language"], "perl"); + assert_eq!(report["comments"].as_array().unwrap().len(), 1); + assert_eq!(report["comments"][0]["span"]["start"], 64); + assert_eq!(report["comments"][0]["span"]["end"], 74); + + let fixed = run(directory.path(), &["fix", "script.pl"]); + assert_eq!( + fixed.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&fixed.stderr) + ); + assert_eq!( + fs::read(&path).unwrap(), + b"=head1 NAME\n# not a comment\n=cut\nmy $x = 'a#b';\nmy $y = $x / 2; \n" + ); +} diff --git a/spec/config.schema.json b/spec/config.schema.json index 8cacf77..7e64936 100644 --- a/spec/config.schema.json +++ b/spec/config.schema.json @@ -70,7 +70,7 @@ "$defs": { "strings": { "type": "array", "items": { "type": "string" }, "default": [] }, "policy": { "enum": ["safe", "legal", "all"], "default": "safe" }, - "language": { "enum": ["rust", "ocaml", "c", "cpp", "go", "java", "javascript", "typescript", "python", "shell", "html", "css", "jsonc", "sql", "kotlin", "toml", "lua", "yaml", "php", "ruby", "zig", "r", "dart", "swift", "csharp", "scala", "vue", "svelte", "markdown"] }, + "language": { "enum": ["rust", "ocaml", "c", "cpp", "go", "java", "javascript", "typescript", "python", "shell", "html", "css", "jsonc", "sql", "kotlin", "toml", "lua", "yaml", "php", "ruby", "zig", "r", "dart", "swift", "csharp", "scala", "vue", "svelte", "markdown", "perl"] }, "kind": { "enum": ["line", "block", "doc-line", "doc-block", "directive", "license", "html-comment", "shebang", "encoding", "optimizer-hint", "version-comment"] }, "kinds": { "type": "array", "items": { "$ref": "#/$defs/kind" }, "default": [] }, "languageConfig": { diff --git a/spec/fixtures/v1/builtins.json b/spec/fixtures/v1/builtins.json index 59dd37e..4301b22 100644 --- a/spec/fixtures/v1/builtins.json +++ b/spec/fixtures/v1/builtins.json @@ -1656,6 +1656,30 @@ "diagnostics": [], "output_utf8": "text\n\nmore\n```rust\n\n```\n`// inline`\n" } + }, + { + "id": "perl-builtin-safe", + "language": "perl", + "operation": "transform", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "=head1 NAME\n# not a comment\n=cut\nmy $x = 'a#b';\nif ($x =~ /a#b/) { print \"yes\\n\" }\nmy $y = $x / 2; # division\n", + "note": "Perl 5.38: the POD block and the string and regex are opaque \u2014 the `#` inside them is content \u2014 and `$x / 2` is a division whose `# division` is a comment. Ground truth, `perl -c`: the source compiles.", + "expect": { + "valid": true, + "comments": [ + { + "start": 99, + "end": 109, + "kind": "line", + "action": "remove" + } + ], + "diagnostics": [], + "output_utf8": "=head1 NAME\n# not a comment\n=cut\nmy $x = 'a#b';\nif ($x =~ /a#b/) { print \"yes\\n\" }\nmy $y = $x / 2; \n" + } } ] } diff --git a/spec/fixtures/v1/floor.txt b/spec/fixtures/v1/floor.txt index 2ec6beb..3f36a2d 100644 --- a/spec/fixtures/v1/floor.txt +++ b/spec/fixtures/v1/floor.txt @@ -16,5 +16,5 @@ # Blank lines and `#` lines are ignored; every other line is a name and a # decimal count separated by white space. -cases 464 -expectations 464 +cases 466 +expectations 466 diff --git a/spec/fixtures/v1/hazards.json b/spec/fixtures/v1/hazards.json index c92cf01..6ee8281 100644 --- a/spec/fixtures/v1/hazards.json +++ b/spec/fixtures/v1/hazards.json @@ -11166,6 +11166,33 @@ "comments": [], "diagnostics": [] } + }, + { + "id": "perl-ambiguous-slash-after-paren", + "language": "perl", + "operation": "scan", + "options": { + "policy": "safe", + "layout": "lines" + }, + "source_utf8": "sub f { 1 }\nf() /a#b/;\nmy $x = (2) / 2; # division\n", + "note": "Perl 5.38: a `/` directly after a closing parenthesis is settled only by the parse context \u2014 `f() /a#b/` is a regex whose `#` is pattern content, and `(2) / 2` is a division whose `# division` is a comment \u2014 so this scanner reports it as lexically ambiguous, marks the file invalid and refuses to edit it. Ground truth, `perl -c`: both forms compile.", + "expect": { + "valid": false, + "comments": [], + "diagnostics": [ + { + "code": "lexical-ambiguity", + "start": 16, + "end": 21 + }, + { + "code": "lexical-ambiguity", + "start": 35, + "end": 51 + } + ] + } } ] } diff --git a/spec/languages.toml b/spec/languages.toml index a06d499..7331623 100644 --- a/spec/languages.toml +++ b/spec/languages.toml @@ -245,3 +245,10 @@ name = "markdown" extensions = ["md", "markdown", "rmd"] dialects = ["standard"] notes = "CommonMark documents; HTML comments are comments, fenced code blocks are scanned as the language their info string names, and inline and indented code are opaque" + +[[languages]] +name = "perl" +extensions = ["pl", "pm", "t"] +dialects = ["standard"] +shebangs = ["perl"] +notes = "Perl 5.38 lexical forms; quote words and regexes hide a `#`, POD blocks are opaque, and a `/` the parse context alone settles is reported as lexically ambiguous" diff --git a/spec/result.schema.json b/spec/result.schema.json index b904837..fa78af2 100644 --- a/spec/result.schema.json +++ b/spec/result.schema.json @@ -132,7 +132,7 @@ } }, "language": { - "enum": ["rust", "ocaml", "c", "cpp", "go", "java", "javascript", "typescript", "python", "shell", "html", "css", "jsonc", "sql", "kotlin", "toml", "lua", "yaml", "php", "ruby", "zig", "r", "dart", "swift", "csharp", "scala", "vue", "svelte", "markdown", "unknown"] + "enum": ["rust", "ocaml", "c", "cpp", "go", "java", "javascript", "typescript", "python", "shell", "html", "css", "jsonc", "sql", "kotlin", "toml", "lua", "yaml", "php", "ruby", "zig", "r", "dart", "swift", "csharp", "scala", "vue", "svelte", "markdown", "perl", "unknown"] }, "kind": { "enum": ["line", "block", "doc-line", "doc-block", "directive", "license", "html-comment", "shebang", "encoding", "optimizer-hint", "version-comment"] diff --git a/tools/fuzz_differential.py b/tools/fuzz_differential.py index b9f2779..0fde06e 100755 --- a/tools/fuzz_differential.py +++ b/tools/fuzz_differential.py @@ -215,6 +215,12 @@ "// c", "/* c */", "a {", "}", ] +PERL_STRUCTURE = [ + "#", "q{", "q}", "qq{", "qw(", "qx`", "m/", "s/", "tr{", "y{", "/", + "<<'EOF'", "<", + "$x", "@y", "%h", "# not", "#c", "\\", "(" , ")", "{", "}", "[", "]", +] + # NOTE: The bytes a lexer is liable to mishandle: NUL, DEL, a byte order mark, a # NOTE: no-break space, the two Unicode line terminators, and two characters # NOTE: wider than one byte. @@ -243,6 +249,7 @@ + VUE_STRUCTURE + SVELTE_STRUCTURE + SCSS_STRUCTURE + + PERL_STRUCTURE + AWKWARD_BYTES ) diff --git a/tools/gen_docs.py b/tools/gen_docs.py index 0f6a307..bfe839b 100644 --- a/tools/gen_docs.py +++ b/tools/gen_docs.py @@ -578,6 +578,17 @@ def languages_page() -> str: "inline code span or an indented code block is opaque, so a `//` or", "a `/*` inside one is code text, not a comment.", "", + "Perl is scanned conservatively: a `#` runs to the end of its", + "line, a POD block is opaque, and every quote word — the single", + "and double quotes and backticks, `q`, `qq`, `qw`, `qx`, `m`,", + "`s`, `tr` and `y` with delimiters of their own, and the", + "here-documents — hides a `#` written inside it. A `/` directly", + "after a closing parenthesis, bracket or brace is reported as", + "lexically ambiguous: perl reads `f() /a#b/` as a regular", + "expression and `(2) / 2` as a division, and only the parse", + "context tells which, so the file is called invalid and nothing", + "is edited.", + "", "YAML is scanned lexically, and `valid` is a lexical answer: the", "shapes a YAML *parser* rejects are not all shapes a lexer can see.", "A comment line inside a multi-line plain scalar makes the file a", From 325270b49b67d897e6642b3cefe387da0e468da4 Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Fri, 28 Aug 2026 08:20:35 +0900 Subject: [PATCH 19/21] ci: fit the workflows to the repository's selected-actions policy The repository allows only GitHub-owned actions, the SHA-pinned tools on its allowlist (rust-toolchain, setup-ocaml), and local actions, so the docker job's setup-buildx/build-push steps and the docs workflow's install-action step could not resolve at workflow start, failing both runs before a single job ran. - docker: build with plain docker, and the hosted runner's default buildx builder for the release-path build - docs: fetch the pinned mdBook release archive directly instead of installing it through an action --- .github/workflows/ci.yml | 16 +++++----------- .github/workflows/docs.yml | 16 +++++++++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4da5e0..6cf09dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -285,19 +285,13 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 # A source build on one platform, which is the path a release never # takes, so the Dockerfile's own builder stage cannot rot between # releases. The step after the smoke test takes the release path over the # same file. - name: Build the image from source - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 - with: - context: . - platforms: linux/amd64 - push: false - load: true - tags: ocomment:ci + shell: bash + run: docker build -t ocomment:ci . - name: Smoke test the image shell: bash run: | @@ -315,9 +309,9 @@ jobs: # stage with a buildx named context holding the musl binaries the release # matrix already built. Handing the image its own binary back through # that context exercises the second path over the same Dockerfile, so a - # release build is never the first to find the layout broken. The - # `docker/setup-buildx-action` step above is what makes `--build-context` - # available; this step uses that same builder. + # release build is never the first to find the layout broken. The hosted + # runner's default buildx builder supplies `--build-context`; this step + # uses that same builder. - name: Build the image again through the release path shell: bash run: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 09dbf6d..7aa5789 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -35,11 +35,17 @@ jobs: with: persist-credentials: false - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable - - uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc # v2 - with: - # Pinned: mdBook decides the rendered HTML, so an unpinned tool would - # let the published site change under a commit that touched nothing. - tool: mdbook@0.5.4 + # Pinned: mdBook decides the rendered HTML, so an unpinned tool would + # let the published site change under a commit that touched nothing. + # The archive is fetched by hand because the repository's action policy + # does not allow third-party actions outside its allowlist. + - name: Install mdBook 0.5.4 + shell: bash + run: | + set -euo pipefail + curl -fsSL https://github.com/rust-lang/mdBook/releases/download/v0.5.4/mdbook-v0.5.4-x86_64-unknown-linux-gnu.tar.gz \ + | tar -xz -C /usr/local/bin + mdbook --version - run: cargo build --manifest-path rust/Cargo.toml --locked -p ocomment # The site may not restate anything the binary or spec/ no longer says. - run: python3 tools/gen_docs.py --check From e4cc7332db478e9b787e9fe58b136016906c81d8 Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:00:29 +0900 Subject: [PATCH 20/21] fix: sound tag-boundary restart rule; tag the docs the dogfood gate found The CI run for PR #13 surfaced three failures: 1. incremental: an edit that closes a tag the earlier scan had read as text left the engine offering checkpoints the full scan refuses. The tag-boundary rule only walked back to the nearest '<', but a tag's attributes can hold '<' of their own (a quote pair in reads the second '<' as text), so a checkpoint could stand inside the outer tag once the edit supplied its '>'. The rule now reads every '<' before the offset and refuses when any of them is still open there; the engine re-asks the same rule when it picks a restart point, so the edited document's checkpoint list agrees with a fresh full scan. Regression test: an_edit_that_closes_a_tag_withdraws_the_checkpoints_inside_it. 2. dogfood: 22 untagged explanatory comments in the docs and the policies sample, tagged per the repository convention (NOTE). 3. vscode: the manifest unit test still pinned the language count at 24; the extension contributes 35 now. --- CONTRIBUTING.md | 10 +++--- README.md | 2 +- docs/ci.md | 8 ++--- docs/configuration.md | 4 +-- docs/library.md | 14 ++++----- docs/policies.md | 8 ++--- editors/vscode/README.md | 2 +- editors/vscode/src/test/unit/manifest.test.ts | 2 +- rust/ocomment-core/src/incremental.rs | 31 +++++++++++++++++++ rust/ocomment-core/src/scanner.rs | 25 ++++++++++----- tools/gen_docs.py | 4 +-- 11 files changed, 75 insertions(+), 35 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af4aa20..bb1bf81 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -98,9 +98,9 @@ shapes a lexer cannot rule out and a parser will not take. ```sh python3 -m pip install pyyaml cargo build --manifest-path rust/Cargo.toml --locked -p ocomment -python3 tools/yaml_roundtrip.py # the full sweep -python3 tools/yaml_roundtrip.py --cases 200 # what CI runs -python3 tools/yaml_roundtrip.py --cases 20000 --seed 7 # a longer sweep +python3 tools/yaml_roundtrip.py # NOTE: the full sweep +python3 tools/yaml_roundtrip.py --cases 200 # NOTE: what CI runs +python3 tools/yaml_roundtrip.py --cases 20000 --seed 7 # NOTE: a longer sweep ``` CI runs `python3 tools/yaml_roundtrip.py --cases 200` in the `dogfood` job: the @@ -125,8 +125,8 @@ differed once, with a shrunken source that still shows it. ```sh cargo build --manifest-path rust/Cargo.toml -p ocomment-core --example ref_driver --locked opam exec -- dune build --root ocaml bin/main.exe -python3 tools/fuzz_differential.py --seed 1 --seed 2 # ~2 minutes -python3 tools/fuzz_differential.py --cases 200 # a quicker sweep +python3 tools/fuzz_differential.py --seed 1 --seed 2 # NOTE: ~2 minutes +python3 tools/fuzz_differential.py --cases 200 # NOTE: a quicker sweep ``` The pool it draws from is one pool for every language, so a scanner meets the diff --git a/README.md b/README.md index eed9536..d85cd58 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ published). ```sh cargo install ocomment --locked -ocomment # check the current directory +ocomment # NOTE: check the current directory ocomment check src tests ocomment diff src ocomment fix --dry-run src diff --git a/docs/ci.md b/docs/ci.md index b28e35e..aa43bd3 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -134,8 +134,8 @@ itself only complains when a consumer runs the hook. CI runs it next to `tools/check_embedded_specs.py`. ```sh -python3 tools/check_hooks.py # fail on drift -python3 tools/check_hooks.py --print-pattern # the regex the hooks must carry +python3 tools/check_hooks.py # NOTE: fail on drift +python3 tools/check_hooks.py --print-pattern # NOTE: the regex the hooks must carry ``` ## GitHub Action @@ -175,14 +175,14 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - security-events: write # Upload the SARIF file to code scanning. + security-events: write # NOTE: Upload the SARIF file to code scanning. steps: - uses: actions/checkout@v7 - uses: P4suta/OComment@v0.1.0 with: format: sarif upload-sarif: "true" - fail-on-findings: "false" # Let the code-scanning alerts carry the result. + fail-on-findings: "false" # NOTE: Let the code-scanning alerts carry the result. ``` `upload-sarif: "true"` requires `format: sarif`; any other format is a usage diff --git a/docs/configuration.md b/docs/configuration.md index 7950120..b904d90 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -21,8 +21,8 @@ include = [] exclude = ["vendor/**"] [policy] -mode = "safe" # safe, legal, all -layout = "lines" # lines, columns, compact +mode = "safe" # NOTE: safe, legal, all +layout = "lines" # NOTE: lines, columns, compact keep_kind = ["directive"] remove_kind = [] keep_regex = ["(?i)generated"] diff --git a/docs/library.md b/docs/library.md index 94a9adf..0252778 100644 --- a/docs/library.md +++ b/docs/library.md @@ -46,7 +46,7 @@ assert!(report.comments[0].disposition.is_remove()); ```rust use ocomment_core::{Language, TransformOptions, transform}; -// A BOM, a CRLF ending, and a comment to take out. +// NOTE: A BOM, a CRLF ending, and a comment to take out. let source = "\u{feff}fn main() {} // trailing\r\n".as_bytes(); let result = transform(source, Language::Rust, TransformOptions::default()); assert_eq!(result.output, "\u{feff}fn main() {} \r\n".as_bytes()); @@ -58,7 +58,7 @@ use ocomment_core::{Language, TransformOptions, apply_edits, transform}; let source = b"let x = 1; // note\nlet y = 2; // and\n"; let result = transform(source, Language::Rust, TransformOptions::default()); -// Sorted and non-overlapping, so one pass applies them. +// NOTE: Sorted and non-overlapping, so one pass applies them. assert!( result .edits @@ -143,8 +143,8 @@ let source = b"k: |\n a\n# ends the block\n # yamllint disable\nz: 1\n"; let options = ScanOptions::default(); let report = scan(source, Language::Yaml, options.clone()); -// The scan kept the first comment: removing its line would hand the directive -// under it back to the block scalar above. +// NOTE: The scan kept the first comment: removing its line would hand the directive +// NOTE: under it back to the block scalar above. let comment = &report.comments[0]; let why = explain_comment( comment, @@ -182,7 +182,7 @@ assert_eq!(found.language, Language::TypeScript); assert_eq!(found.dialect, Dialect::Tsx); assert_eq!(found.reason, "extension"); -// No name, so the shebang decides. +// NOTE: No name, so the shebang decides. let piped = detect_language(None, b"#!/usr/bin/env python3\n").unwrap(); assert_eq!(piped.language, Language::Python); ``` @@ -212,7 +212,7 @@ let result = transform_spans( TransformOptions::default(), ) .unwrap(); -// The same policy the built-in scanners get: the directive is kept. +// NOTE: The same policy the built-in scanners get: the directive is kept. assert_eq!(result.output, b"a b/* directive */"); let bad = transform_spans( @@ -286,7 +286,7 @@ let mut document = IncrementalDocument::new( 1, ); -// A span past the end of the document is refused, and nothing moves. +// NOTE: A span past the end of the document is refused, and nothing moves. let outside = ByteSpan::new(0, document.source().len() + 1); assert_eq!( document.apply_changes( diff --git a/docs/policies.md b/docs/policies.md index df8d4f1..8d346e0 100644 --- a/docs/policies.md +++ b/docs/policies.md @@ -21,8 +21,8 @@ spaces included. // rustfmt::skip /// Adds two numbers. pub fn add(a: u32, b: u32) -> u32 { - let total = a + /* widen */ b; // TODO: check for overflow - /* Everything from here down is one block comment + let total = a + /* NOTE: widen */ b; // TODO: check for overflow + /* NOTE: Everything from here down is one block comment that runs across three lines, so each layout has something to show. */ total @@ -121,8 +121,8 @@ pub fn add(a: u32, b: u32) -> u32 { // rustfmt::skip pub fn add(a: u32, b: u32) -> u32 { - let total = a + b; - + let total = a + b; + total diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 365f61f..69bfc00 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -16,7 +16,7 @@ bundle one. Install the binary and make sure it is on your `PATH`: ```sh cargo install ocomment --locked -# or: docker pull ghcr.io/p4suta/ocomment +# NOTE: or: docker pull ghcr.io/p4suta/ocomment ``` Release archives for Linux, macOS, and Windows are attached to every diff --git a/editors/vscode/src/test/unit/manifest.test.ts b/editors/vscode/src/test/unit/manifest.test.ts index bfe0347..ae688dc 100644 --- a/editors/vscode/src/test/unit/manifest.test.ts +++ b/editors/vscode/src/test/unit/manifest.test.ts @@ -52,7 +52,7 @@ test("every language the extension attaches to also activates it", () => { // NOTE: docs/editors.md, both changelogs -- is checked against this same list // NOTE: by `every_written_language_count_matches_what_it_counts` in // NOTE: rust/ocomment/tests/spec_languages.rs. - assert.equal(configured.length, 24); + assert.equal(configured.length, 35); // NOTE: A workspace can hold a configuration file and no open editor, and // NOTE: the status bar and the workspace fix have to work there too. assert.ok( diff --git a/rust/ocomment-core/src/incremental.rs b/rust/ocomment-core/src/incremental.rs index d1f2699..2b8b367 100644 --- a/rust/ocomment-core/src/incremental.rs +++ b/rust/ocomment-core/src/incremental.rs @@ -809,6 +809,37 @@ mod tests { assert_eq!(document.safe_checkpoints(), expected_checkpoints); } + /// Regression: an edit that closes a tag the earlier scan had read as text + /// must withdraw every checkpoint the old reading offered inside it. A + /// tag's attributes can hold `<` of their own — `
`; every `<` before the offset has to close. + #[test] + fn an_edit_that_closes_a_tag_withdraws_the_checkpoints_inside_it() { + let source = b"
\"\nline2\n".to_vec(); + assert_eq!(source.len(), 19); + let mut document = + IncrementalDocument::new(source, Language::Vue, ScanOptions::default(), 1); + /* NOTE: the span is the document's end, and the replacement is the + * `>` the outer tag had been missing, which closes the tag across + * both checkpoints the unclosed tag had let stand. */ + document + .apply_changes( + &[DocumentChange { + span: ByteSpan::new(19, 19), + replacement: b">".to_vec(), + }], + 2, + ) + .unwrap(); + let (expected, expected_checkpoints) = + scan_with_checkpoints(document.source(), Language::Vue, ScanOptions::default(), 0); + assert_eq!(document.report().comments, expected.comments); + assert_eq!(document.report(), &expected); + assert_eq!(document.safe_checkpoints(), expected_checkpoints); + } + /// A here document body and an embedded document are two Ruby states whose /// lines say nothing about themselves: the `#` at the head of one is a byte /// of the value, and the line that decides so sits above it. Neither offers diff --git a/rust/ocomment-core/src/scanner.rs b/rust/ocomment-core/src/scanner.rs index d54dab7..4c538a3 100644 --- a/rust/ocomment-core/src/scanner.rs +++ b/rust/ocomment-core/src/scanner.rs @@ -6004,13 +6004,18 @@ impl RestartRules { /// would read the rest of the tag as ordinary text and find a comment in it /// the full scan protected. /// -/// The test walks back to the nearest `<` and reads the tag forward from it, +/// The test walks back from `offset` and reads every `<` it meets forward, /// exactly as the scan would: the quotes pair and the first unquoted `>` -/// ends the tag. The offset is refused when the tag is still open there — its -/// `>` lies beyond it or never comes. A suffix scan — whose own source begins -/// at the checkpoint — gives the same answer, because a checkpoint that is -/// itself sound, as every recorded one is, sits inside no tag, so the `<` -/// its walk meets lies within its bytes. +/// ends the tag. The offset is refused when any of those tags is still open +/// there — its `>` lies beyond it or never comes. Every `<` rather than only +/// the nearest one, because a tag's attributes may hold further `<` signs of +/// their own: the tag that decided the scan's reading of this stretch can +/// open far behind the closest sign, and an edit that grows the tag's `>` +/// across the offset can withdraw a checkpoint the nearest sign alone would +/// still permit. A suffix scan — whose own source begins at the checkpoint — +/// gives the same answer, because a checkpoint that is itself sound, as every +/// recorded one is, sits inside no tag, so every `<` its walk meets lies +/// within its bytes. fn the_tag_boundary_permits_a_restart(source: &[u8], offset: usize) -> bool { let mut index = offset; while index > 0 { @@ -6018,6 +6023,7 @@ fn the_tag_boundary_permits_a_restart(source: &[u8], offset: usize) -> bool { if source[index] == b'<' { let mut cursor = index + 1; let mut quote = None; + let mut closed = false; while cursor < offset { if let Some(active) = quote { if source[cursor] == active { @@ -6026,11 +6032,14 @@ fn the_tag_boundary_permits_a_restart(source: &[u8], offset: usize) -> bool { } else if matches!(source[cursor], b'\'' | b'"') { quote = Some(source[cursor]); } else if source[cursor] == b'>' { - return true; + closed = true; + break; } cursor += 1; } - return false; + if !closed { + return false; + } } } true diff --git a/tools/gen_docs.py b/tools/gen_docs.py index bfe839b..01733dc 100644 --- a/tools/gen_docs.py +++ b/tools/gen_docs.py @@ -657,8 +657,8 @@ def languages_page() -> str: // rustfmt::skip /// Adds two numbers. pub fn add(a: u32, b: u32) -> u32 { - let total = a + /* widen */ b; // TODO: check for overflow - /* Everything from here down is one block comment + let total = a + /* NOTE: widen */ b; // TODO: check for overflow + /* NOTE: Everything from here down is one block comment that runs across three lines, so each layout has something to show. */ total From 2b5800115998e92bbe453f81baf7e5a37daf9775 Mon Sep 17 00:00:00 2001 From: Yasunobu <42543015+P4suta@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:09:25 +0900 Subject: [PATCH 21/21] ci: carry the spec corpus into the self-strip copy The dogfood job strips a copy of rust/ and builds it, but the crate's spec-fixture tests read the corpus from ../../spec, which the copy never received. Copy spec/ alongside, and exclude the fixtures from the strip: they are the tests' input, not source to rewrite. --- .github/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cf09dd..156b5d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,10 +150,13 @@ jobs: set -euo pipefail rm -rf "${RUNNER_TEMP}/strip" mkdir -p "${RUNNER_TEMP}/strip" - git ls-files -z rust | xargs -0 cp --parents -t "${RUNNER_TEMP}/strip" + git ls-files -z rust spec | xargs -0 cp --parents -t "${RUNNER_TEMP}/strip" # The copy inherits no configuration of its own, and the patched - # crate under rust/vendor is not ours to rewrite. - printf 'version = 1\n\n[files]\nexclude = ["rust/vendor/**"]\n' \ + # crate under rust/vendor is not ours to rewrite. The spec corpus + # travels with the copy because the crate's spec-fixture tests read + # it from `../../spec`; it is the tests' input, so it is excluded + # from the strip rather than rewritten by it. + printf 'version = 1\n\n[files]\nexclude = ["rust/vendor/**", "spec/fixtures/**"]\n' \ >"${RUNNER_TEMP}/strip/.ocomment.toml" cd "${RUNNER_TEMP}/strip" "${GITHUB_WORKSPACE}/rust/target/debug/ocomment" \