From 02a940247260dfe29f8e378b2ca8471a50a2ada6 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 5 Dec 2025 02:49:47 +0900 Subject: [PATCH] feat(hooks): add comment-checker hook for detecting unnecessary comments - Port Go comment-checker to TypeScript using web-tree-sitter - Support 38 programming languages via tree-sitter-wasms - Filter out valid comments: BDD patterns, lint directives, docstrings, shebangs - Integrate with OpenCode's tool.execute.before/after hooks - Attach feedback to Write/Edit/MultiEdit tool output --- bun.lock | 29 +++ package.json | 17 +- src/hooks/comment-checker/constants.ts | 204 ++++++++++++++++++ src/hooks/comment-checker/detector.ts | 142 ++++++++++++ src/hooks/comment-checker/filters/bdd.ts | 21 ++ .../comment-checker/filters/directive.ts | 24 +++ .../comment-checker/filters/docstring.ts | 12 ++ src/hooks/comment-checker/filters/index.ts | 26 +++ src/hooks/comment-checker/filters/shebang.ts | 9 + src/hooks/comment-checker/index.ts | 101 +++++++++ src/hooks/comment-checker/output/formatter.ts | 11 + src/hooks/comment-checker/output/index.ts | 2 + .../comment-checker/output/xml-builder.ts | 24 +++ src/hooks/comment-checker/types.ts | 36 ++++ src/hooks/index.ts | 1 + 15 files changed, 655 insertions(+), 4 deletions(-) create mode 100644 src/hooks/comment-checker/constants.ts create mode 100644 src/hooks/comment-checker/detector.ts create mode 100644 src/hooks/comment-checker/filters/bdd.ts create mode 100644 src/hooks/comment-checker/filters/directive.ts create mode 100644 src/hooks/comment-checker/filters/docstring.ts create mode 100644 src/hooks/comment-checker/filters/index.ts create mode 100644 src/hooks/comment-checker/filters/shebang.ts create mode 100644 src/hooks/comment-checker/index.ts create mode 100644 src/hooks/comment-checker/output/formatter.ts create mode 100644 src/hooks/comment-checker/output/index.ts create mode 100644 src/hooks/comment-checker/output/xml-builder.ts create mode 100644 src/hooks/comment-checker/types.ts diff --git a/bun.lock b/bun.lock index ffd912e..db67f64 100644 --- a/bun.lock +++ b/bun.lock @@ -5,8 +5,12 @@ "": { "name": "oh-my-opencode", "dependencies": { + "@ast-grep/cli": "^0.40.0", "@ast-grep/napi": "^0.40.0", "@opencode-ai/plugin": "^1.0.7", + "tree-sitter-wasms": "^0.1.12", + "web-tree-sitter": "^0.24.7", + "zod": "^4.1.8", }, "devDependencies": { "bun-types": "latest", @@ -17,7 +21,26 @@ }, }, }, + "trustedDependencies": [ + "@ast-grep/cli", + ], "packages": { + "@ast-grep/cli": ["@ast-grep/cli@0.40.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.40.0", "@ast-grep/cli-darwin-x64": "0.40.0", "@ast-grep/cli-linux-arm64-gnu": "0.40.0", "@ast-grep/cli-linux-x64-gnu": "0.40.0", "@ast-grep/cli-win32-arm64-msvc": "0.40.0", "@ast-grep/cli-win32-ia32-msvc": "0.40.0", "@ast-grep/cli-win32-x64-msvc": "0.40.0" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-L8AkflsfI2ZP70yIdrwqvjR02ScCuRmM/qNGnJWUkOFck+e6gafNVJ4e4jjGQlEul+dNdBpx36+O2Op629t47A=="], + + "@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UehY2MMUkdJbsriP7NKc6+uojrqPn7d1Cl0em+WAkee7Eij81VdyIjRsRxtZSLh440ZWQBHI3PALZ9RkOO8pKQ=="], + + "@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-RFDJ2ZxUbT0+grntNlOLJx7wa9/ciVCeaVtQpQy8WJJTvXvkY0etl8Qlh2TmO2x2yr+i0Z6aMJi4IG/Yx5ghTQ=="], + + "@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4p55gnTQ1mMFCyqjtM7bH9SB9r16mkwXtUcJQGX1YgFG4WD+QG8rC4GwSuNNZcdlYaOQuTWrgUEQ9z5K06UXfg=="], + + "@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-u2MXFceuwvrO+OQ6zFGoJ6wbATXn46HWwW79j4UPrXYJzVl97jRyjJOIQTJOzTflsk02fjP98DQkfvbXt2dl3Q=="], + + "@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-E/I1xpF/RQL2fo1CQsQfTxyDLnChsbZ+ERrQHKuF1FI4WrkaPOBibpqda60QgVmUcgOGZyZ/GRb3iKEVWPsQNQ=="], + + "@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-9h12OQu1BR0GxHEtT+Z4QkJk3LLWLiKwjBkjXUGlASHYDPTyLcs85KwDLeFHs4BwarF8TDdF+KySvB9WPGl/nQ=="], + + "@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-n2+3WynEWFHhXg6KDgjwWQ0UEtIvqUITFbKEk5cDkUYrzYhg/A6kj0qauPwRbVMoJms49vtsNpLkzzqyunio5g=="], + "@ast-grep/napi": ["@ast-grep/napi@0.40.0", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.0", "@ast-grep/napi-darwin-x64": "0.40.0", "@ast-grep/napi-linux-arm64-gnu": "0.40.0", "@ast-grep/napi-linux-arm64-musl": "0.40.0", "@ast-grep/napi-linux-x64-gnu": "0.40.0", "@ast-grep/napi-linux-x64-musl": "0.40.0", "@ast-grep/napi-win32-arm64-msvc": "0.40.0", "@ast-grep/napi-win32-ia32-msvc": "0.40.0", "@ast-grep/napi-win32-x64-msvc": "0.40.0" } }, "sha512-tq6nO/8KwUF/mHuk1ECaAOSOlz2OB/PmygnvprJzyAHGRVzdcffblaOOWe90M9sGz5MAasXoF+PTcayQj9TKKA=="], "@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZMjl5yLhKjxdwbqEEdMizgQdWH2NrWsM6Px+JuGErgCDe6Aedq9yurEPV7veybGdLVJQhOah6htlSflXxjHnYA=="], @@ -70,10 +93,16 @@ "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "tree-sitter-wasms": ["tree-sitter-wasms@0.1.13", "", { "dependencies": { "tree-sitter-wasms": "^0.1.11" } }, "sha512-wT+cR6DwaIz80/vho3AvSF0N4txuNx/5bcRKoXouOfClpxh/qqrF4URNLQXbbt8MaAxeksZcZd1j8gcGjc+QxQ=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "web-tree-sitter": ["web-tree-sitter@0.24.7", "", {}, "sha512-CdC/TqVFbXqR+C51v38hv6wOPatKEUGxa39scAeFSm98wIhZxAYonhRQPSMmfZ2w7JDI0zQDdzdmgtNk06/krQ=="], + "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], } } diff --git a/package.json b/package.json index d909910..b056db6 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,12 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" - } + }, + "./schema.json": "./dist/oh-my-opencode.schema.json" }, "scripts": { - "build": "bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi && tsc --emitDeclarationOnly", + "build": "bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi && tsc --emitDeclarationOnly && bun run build:schema", + "build:schema": "bun run script/build-schema.ts", "clean": "rm -rf dist", "prepublishOnly": "bun run clean && bun run build", "typecheck": "tsc --noEmit" @@ -40,8 +42,12 @@ }, "homepage": "https://github.com/code-yeongyu/oh-my-opencode#readme", "dependencies": { + "@ast-grep/cli": "^0.40.0", "@ast-grep/napi": "^0.40.0", - "@opencode-ai/plugin": "^1.0.7" + "@opencode-ai/plugin": "^1.0.7", + "tree-sitter-wasms": "^0.1.12", + "web-tree-sitter": "^0.24.7", + "zod": "^4.1.8" }, "devDependencies": { "bun-types": "latest", @@ -49,5 +55,8 @@ }, "peerDependencies": { "bun": ">=1.0.0" - } + }, + "trustedDependencies": [ + "@ast-grep/cli" + ] } diff --git a/src/hooks/comment-checker/constants.ts b/src/hooks/comment-checker/constants.ts new file mode 100644 index 0000000..b51f51e --- /dev/null +++ b/src/hooks/comment-checker/constants.ts @@ -0,0 +1,204 @@ +import type { LanguageConfig } from "./types" + +export const EXTENSION_TO_LANGUAGE: Record = { + py: "python", + js: "javascript", + jsx: "javascript", + ts: "typescript", + tsx: "tsx", + go: "golang", + java: "java", + kt: "kotlin", + scala: "scala", + c: "c", + h: "c", + cpp: "cpp", + cc: "cpp", + cxx: "cpp", + hpp: "cpp", + rs: "rust", + rb: "ruby", + sh: "bash", + bash: "bash", + cs: "csharp", + swift: "swift", + ex: "elixir", + exs: "elixir", + lua: "lua", + php: "php", + ml: "ocaml", + mli: "ocaml", + sql: "sql", + html: "html", + htm: "html", + css: "css", + yaml: "yaml", + yml: "yaml", + toml: "toml", + hcl: "hcl", + tf: "hcl", + dockerfile: "dockerfile", + proto: "protobuf", + svelte: "svelte", + elm: "elm", + groovy: "groovy", + cue: "cue", +} + +export const QUERY_TEMPLATES: Record = { + python: "(comment) @comment", + javascript: "(comment) @comment", + typescript: "(comment) @comment", + tsx: "(comment) @comment", + golang: "(comment) @comment", + rust: ` + (line_comment) @comment + (block_comment) @comment + `, + kotlin: ` + (line_comment) @comment + (multiline_comment) @comment + `, + java: ` + (line_comment) @comment + (block_comment) @comment + `, + c: "(comment) @comment", + cpp: "(comment) @comment", + csharp: "(comment) @comment", + ruby: "(comment) @comment", + bash: "(comment) @comment", + swift: "(comment) @comment", + elixir: "(comment) @comment", + lua: "(comment) @comment", + php: "(comment) @comment", + ocaml: "(comment) @comment", + sql: "(comment) @comment", + html: "(comment) @comment", + css: "(comment) @comment", + yaml: "(comment) @comment", + toml: "(comment) @comment", + hcl: "(comment) @comment", + dockerfile: "(comment) @comment", + protobuf: "(comment) @comment", + svelte: "(comment) @comment", + elm: "(comment) @comment", + groovy: "(comment) @comment", + cue: "(comment) @comment", + scala: "(comment) @comment", +} + +export const DOCSTRING_QUERIES: Record = { + python: ` + (module . (expression_statement (string) @docstring)) + (class_definition body: (block . (expression_statement (string) @docstring))) + (function_definition body: (block . (expression_statement (string) @docstring))) + `, + javascript: ` + (comment) @jsdoc + (#match? @jsdoc "^/\\\\*\\\\*") + `, + typescript: ` + (comment) @jsdoc + (#match? @jsdoc "^/\\\\*\\\\*") + `, + tsx: ` + (comment) @jsdoc + (#match? @jsdoc "^/\\\\*\\\\*") + `, + java: ` + (comment) @javadoc + (#match? @javadoc "^/\\\\*\\\\*") + `, +} + +export const BDD_KEYWORDS = new Set([ + "given", + "when", + "then", + "arrange", + "act", + "assert", + "when & then", + "when&then", +]) + +export const TYPE_CHECKER_PREFIXES = [ + "type:", + "noqa", + "pyright:", + "ruff:", + "mypy:", + "pylint:", + "flake8:", + "pyre:", + "pytype:", + "eslint-disable", + "eslint-enable", + "eslint-ignore", + "prettier-ignore", + "ts-ignore", + "ts-expect-error", + "ts-nocheck", + "clippy::", + "allow(", + "deny(", + "warn(", + "forbid(", + "nolint", + "go:generate", + "go:build", + "go:embed", + "coverage:", + "c8 ignore", + "biome-ignore", + "region", + "endregion", +] + +export const HOOK_MESSAGE_HEADER = `COMMENT/DOCSTRING DETECTED - IMMEDIATE ACTION REQUIRED + +Your recent changes contain comments or docstrings, which triggered this hook. +You need to take immediate action. You must follow the conditions below. +(Listed in priority order - you must always act according to this priority order) + +CRITICAL WARNING: This hook message MUST NEVER be ignored, even if you receive it multiple times. +You MUST provide corresponding explanation or action for EACH occurrence of this message. +Ignoring this message or failing to respond appropriately is strictly prohibited. + +PRIORITY-BASED ACTION GUIDELINES: + +1. This is a comment/docstring that already existed before +\t-> Explain to the user that this is an existing comment/docstring and proceed (justify it) + +2. This is a newly written comment: but it's in given, when, then format +\t-> Tell the user it's a BDD comment and proceed (justify it) +\t-> Note: This applies to comments only, not docstrings + +3. This is a newly written comment/docstring: but it's a necessary comment/docstring +\t-> Tell the user why this comment/docstring is absolutely necessary and proceed (justify it) +\t-> Examples of necessary comments: complex algorithms, security-related, performance optimization, regex, mathematical formulas +\t-> Examples of necessary docstrings: public API documentation, complex module/class interfaces +\t-> IMPORTANT: Most docstrings are unnecessary if the code is self-explanatory. Only keep truly essential ones. + +4. This is a newly written comment/docstring: but it's an unnecessary comment/docstring +\t-> Apologize to the user and remove the comment/docstring. +\t-> Make the code itself clearer so it can be understood without comments/docstrings. +\t-> For verbose docstrings: refactor code to be self-documenting instead of adding lengthy explanations. + +MANDATORY REQUIREMENT: You must acknowledge this hook message and take one of the above actions. +Review in the above priority order and take the corresponding action EVERY TIME this appears. + +Detected comments/docstrings: +` + +export function getLanguageByExtension(filePath: string): string | null { + const lastDot = filePath.lastIndexOf(".") + if (lastDot === -1) { + const baseName = filePath.split("/").pop()?.toLowerCase() + if (baseName === "dockerfile") return "dockerfile" + return null + } + const ext = filePath.slice(lastDot + 1).toLowerCase() + return EXTENSION_TO_LANGUAGE[ext] ?? null +} diff --git a/src/hooks/comment-checker/detector.ts b/src/hooks/comment-checker/detector.ts new file mode 100644 index 0000000..64e74ef --- /dev/null +++ b/src/hooks/comment-checker/detector.ts @@ -0,0 +1,142 @@ +import type { CommentInfo, CommentType } from "./types" +import { getLanguageByExtension, QUERY_TEMPLATES, DOCSTRING_QUERIES } from "./constants" + +export function isSupportedFile(filePath: string): boolean { + return getLanguageByExtension(filePath) !== null +} + +function determineCommentType(text: string, nodeType: string): CommentType { + const stripped = text.trim() + + if (nodeType === "line_comment") { + return "line" + } + if (nodeType === "block_comment" || nodeType === "multiline_comment") { + return "block" + } + + if (stripped.startsWith('"""') || stripped.startsWith("'''")) { + return "docstring" + } + + if (stripped.startsWith("//") || stripped.startsWith("#")) { + return "line" + } + + if (stripped.startsWith("/*") || stripped.startsWith("