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
This commit is contained in:
29
bun.lock
29
bun.lock
@@ -5,8 +5,12 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "oh-my-opencode",
|
"name": "oh-my-opencode",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ast-grep/cli": "^0.40.0",
|
||||||
"@ast-grep/napi": "^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": {
|
"devDependencies": {
|
||||||
"bun-types": "latest",
|
"bun-types": "latest",
|
||||||
@@ -17,7 +21,26 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"trustedDependencies": [
|
||||||
|
"@ast-grep/cli",
|
||||||
|
],
|
||||||
"packages": {
|
"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": ["@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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -12,10 +12,12 @@
|
|||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"import": "./dist/index.js"
|
"import": "./dist/index.js"
|
||||||
}
|
},
|
||||||
|
"./schema.json": "./dist/oh-my-opencode.schema.json"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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",
|
"clean": "rm -rf dist",
|
||||||
"prepublishOnly": "bun run clean && bun run build",
|
"prepublishOnly": "bun run clean && bun run build",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
@@ -40,8 +42,12 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/code-yeongyu/oh-my-opencode#readme",
|
"homepage": "https://github.com/code-yeongyu/oh-my-opencode#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ast-grep/cli": "^0.40.0",
|
||||||
"@ast-grep/napi": "^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": {
|
"devDependencies": {
|
||||||
"bun-types": "latest",
|
"bun-types": "latest",
|
||||||
@@ -49,5 +55,8 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"bun": ">=1.0.0"
|
"bun": ">=1.0.0"
|
||||||
}
|
},
|
||||||
|
"trustedDependencies": [
|
||||||
|
"@ast-grep/cli"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
204
src/hooks/comment-checker/constants.ts
Normal file
204
src/hooks/comment-checker/constants.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import type { LanguageConfig } from "./types"
|
||||||
|
|
||||||
|
export const EXTENSION_TO_LANGUAGE: Record<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
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
|
||||||
|
}
|
||||||
142
src/hooks/comment-checker/detector.ts
Normal file
142
src/hooks/comment-checker/detector.ts
Normal file
@@ -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("<!--") || stripped.startsWith("--")) {
|
||||||
|
return "block"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "line"
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function detectComments(
|
||||||
|
filePath: string,
|
||||||
|
content: string,
|
||||||
|
includeDocstrings = true
|
||||||
|
): Promise<CommentInfo[]> {
|
||||||
|
const langName = getLanguageByExtension(filePath)
|
||||||
|
if (!langName) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryPattern = QUERY_TEMPLATES[langName]
|
||||||
|
if (!queryPattern) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const Parser = (await import("web-tree-sitter")).default
|
||||||
|
await Parser.init()
|
||||||
|
|
||||||
|
const parser = new Parser()
|
||||||
|
|
||||||
|
let wasmPath: string
|
||||||
|
try {
|
||||||
|
const wasmModule = await import(`tree-sitter-wasms/out/tree-sitter-${langName}.wasm`)
|
||||||
|
wasmPath = wasmModule.default
|
||||||
|
} catch {
|
||||||
|
const languageMap: Record<string, string> = {
|
||||||
|
golang: "go",
|
||||||
|
csharp: "c_sharp",
|
||||||
|
cpp: "cpp",
|
||||||
|
}
|
||||||
|
const mappedLang = languageMap[langName] || langName
|
||||||
|
try {
|
||||||
|
const wasmModule = await import(`tree-sitter-wasms/out/tree-sitter-${mappedLang}.wasm`)
|
||||||
|
wasmPath = wasmModule.default
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = await Parser.Language.load(wasmPath)
|
||||||
|
parser.setLanguage(language)
|
||||||
|
|
||||||
|
const tree = parser.parse(content)
|
||||||
|
const comments: CommentInfo[] = []
|
||||||
|
|
||||||
|
const query = language.query(queryPattern)
|
||||||
|
const matches = query.matches(tree.rootNode)
|
||||||
|
|
||||||
|
for (const match of matches) {
|
||||||
|
for (const capture of match.captures) {
|
||||||
|
const node = capture.node
|
||||||
|
const text = node.text
|
||||||
|
const lineNumber = node.startPosition.row + 1
|
||||||
|
|
||||||
|
const commentType = determineCommentType(text, node.type)
|
||||||
|
const isDocstring = commentType === "docstring"
|
||||||
|
|
||||||
|
if (isDocstring && !includeDocstrings) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
comments.push({
|
||||||
|
text,
|
||||||
|
lineNumber,
|
||||||
|
filePath,
|
||||||
|
commentType,
|
||||||
|
isDocstring,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeDocstrings) {
|
||||||
|
const docQuery = DOCSTRING_QUERIES[langName]
|
||||||
|
if (docQuery) {
|
||||||
|
try {
|
||||||
|
const docQueryObj = language.query(docQuery)
|
||||||
|
const docMatches = docQueryObj.matches(tree.rootNode)
|
||||||
|
|
||||||
|
for (const match of docMatches) {
|
||||||
|
for (const capture of match.captures) {
|
||||||
|
const node = capture.node
|
||||||
|
const text = node.text
|
||||||
|
const lineNumber = node.startPosition.row + 1
|
||||||
|
|
||||||
|
const alreadyAdded = comments.some(
|
||||||
|
(c) => c.lineNumber === lineNumber && c.text === text
|
||||||
|
)
|
||||||
|
if (!alreadyAdded) {
|
||||||
|
comments.push({
|
||||||
|
text,
|
||||||
|
lineNumber,
|
||||||
|
filePath,
|
||||||
|
commentType: "docstring",
|
||||||
|
isDocstring: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
comments.sort((a, b) => a.lineNumber - b.lineNumber)
|
||||||
|
|
||||||
|
return comments
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/hooks/comment-checker/filters/bdd.ts
Normal file
21
src/hooks/comment-checker/filters/bdd.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { CommentInfo, FilterResult } from "../types"
|
||||||
|
import { BDD_KEYWORDS } from "../constants"
|
||||||
|
|
||||||
|
function stripCommentPrefix(text: string): string {
|
||||||
|
let stripped = text.trim().toLowerCase()
|
||||||
|
const prefixes = ["#", "//", "--", "/*", "*/"]
|
||||||
|
for (const prefix of prefixes) {
|
||||||
|
if (stripped.startsWith(prefix)) {
|
||||||
|
stripped = stripped.slice(prefix.length).trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stripped
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterBddComments(comment: CommentInfo): FilterResult {
|
||||||
|
const normalized = stripCommentPrefix(comment.text)
|
||||||
|
if (BDD_KEYWORDS.has(normalized)) {
|
||||||
|
return { shouldSkip: true, reason: `BDD keyword: ${normalized}` }
|
||||||
|
}
|
||||||
|
return { shouldSkip: false }
|
||||||
|
}
|
||||||
24
src/hooks/comment-checker/filters/directive.ts
Normal file
24
src/hooks/comment-checker/filters/directive.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { CommentInfo, FilterResult } from "../types"
|
||||||
|
import { TYPE_CHECKER_PREFIXES } from "../constants"
|
||||||
|
|
||||||
|
function stripCommentPrefix(text: string): string {
|
||||||
|
let stripped = text.trim().toLowerCase()
|
||||||
|
const prefixes = ["#", "//", "/*", "--"]
|
||||||
|
for (const prefix of prefixes) {
|
||||||
|
if (stripped.startsWith(prefix)) {
|
||||||
|
stripped = stripped.slice(prefix.length).trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stripped = stripped.replace(/^@/, "")
|
||||||
|
return stripped
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterDirectiveComments(comment: CommentInfo): FilterResult {
|
||||||
|
const normalized = stripCommentPrefix(comment.text)
|
||||||
|
for (const prefix of TYPE_CHECKER_PREFIXES) {
|
||||||
|
if (normalized.startsWith(prefix.toLowerCase())) {
|
||||||
|
return { shouldSkip: true, reason: `Directive: ${prefix}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { shouldSkip: false }
|
||||||
|
}
|
||||||
12
src/hooks/comment-checker/filters/docstring.ts
Normal file
12
src/hooks/comment-checker/filters/docstring.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { CommentInfo, FilterResult } from "../types"
|
||||||
|
|
||||||
|
export function filterDocstringComments(comment: CommentInfo): FilterResult {
|
||||||
|
if (comment.isDocstring) {
|
||||||
|
return { shouldSkip: true, reason: "Docstring" }
|
||||||
|
}
|
||||||
|
const trimmed = comment.text.trimStart()
|
||||||
|
if (trimmed.startsWith("/**")) {
|
||||||
|
return { shouldSkip: true, reason: "JSDoc/PHPDoc" }
|
||||||
|
}
|
||||||
|
return { shouldSkip: false }
|
||||||
|
}
|
||||||
26
src/hooks/comment-checker/filters/index.ts
Normal file
26
src/hooks/comment-checker/filters/index.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { CommentInfo, CommentFilter } from "../types"
|
||||||
|
import { filterBddComments } from "./bdd"
|
||||||
|
import { filterDirectiveComments } from "./directive"
|
||||||
|
import { filterDocstringComments } from "./docstring"
|
||||||
|
import { filterShebangComments } from "./shebang"
|
||||||
|
|
||||||
|
export { filterBddComments, filterDirectiveComments, filterDocstringComments, filterShebangComments }
|
||||||
|
|
||||||
|
const ALL_FILTERS: CommentFilter[] = [
|
||||||
|
filterShebangComments,
|
||||||
|
filterBddComments,
|
||||||
|
filterDirectiveComments,
|
||||||
|
filterDocstringComments,
|
||||||
|
]
|
||||||
|
|
||||||
|
export function applyFilters(comments: CommentInfo[]): CommentInfo[] {
|
||||||
|
return comments.filter((comment) => {
|
||||||
|
for (const filter of ALL_FILTERS) {
|
||||||
|
const result = filter(comment)
|
||||||
|
if (result.shouldSkip) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
9
src/hooks/comment-checker/filters/shebang.ts
Normal file
9
src/hooks/comment-checker/filters/shebang.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { CommentInfo, FilterResult } from "../types"
|
||||||
|
|
||||||
|
export function filterShebangComments(comment: CommentInfo): FilterResult {
|
||||||
|
const trimmed = comment.text.trimStart()
|
||||||
|
if (trimmed.startsWith("#!")) {
|
||||||
|
return { shouldSkip: true, reason: "Shebang" }
|
||||||
|
}
|
||||||
|
return { shouldSkip: false }
|
||||||
|
}
|
||||||
101
src/hooks/comment-checker/index.ts
Normal file
101
src/hooks/comment-checker/index.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import type { PendingCall, FileComments } from "./types"
|
||||||
|
import { detectComments, isSupportedFile } from "./detector"
|
||||||
|
import { applyFilters } from "./filters"
|
||||||
|
import { formatHookMessage } from "./output"
|
||||||
|
|
||||||
|
const pendingCalls = new Map<string, PendingCall>()
|
||||||
|
const PENDING_CALL_TTL = 60_000
|
||||||
|
|
||||||
|
function cleanupOldPendingCalls(): void {
|
||||||
|
const now = Date.now()
|
||||||
|
for (const [callID, call] of pendingCalls) {
|
||||||
|
if (now - call.timestamp > PENDING_CALL_TTL) {
|
||||||
|
pendingCalls.delete(callID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(cleanupOldPendingCalls, 10_000)
|
||||||
|
|
||||||
|
export function createCommentCheckerHooks() {
|
||||||
|
return {
|
||||||
|
"tool.execute.before": async (
|
||||||
|
input: { tool: string; sessionID: string; callID: string },
|
||||||
|
output: { args: Record<string, unknown> }
|
||||||
|
): Promise<void> => {
|
||||||
|
const toolLower = input.tool.toLowerCase()
|
||||||
|
if (toolLower !== "write" && toolLower !== "edit" && toolLower !== "multiedit") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = (output.args.filePath ?? output.args.file_path) as string | undefined
|
||||||
|
const content = output.args.content as string | undefined
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSupportedFile(filePath)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingCalls.set(input.callID, {
|
||||||
|
filePath,
|
||||||
|
content,
|
||||||
|
tool: toolLower as "write" | "edit" | "multiedit",
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
"tool.execute.after": async (
|
||||||
|
input: { tool: string; sessionID: string; callID: string },
|
||||||
|
output: { title: string; output: string; metadata: unknown }
|
||||||
|
): Promise<void> => {
|
||||||
|
const pendingCall = pendingCalls.get(input.callID)
|
||||||
|
if (!pendingCall) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingCalls.delete(input.callID)
|
||||||
|
|
||||||
|
if (output.output.toLowerCase().includes("error")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let content: string
|
||||||
|
|
||||||
|
if (pendingCall.content) {
|
||||||
|
content = pendingCall.content
|
||||||
|
} else {
|
||||||
|
const file = Bun.file(pendingCall.filePath)
|
||||||
|
content = await file.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawComments = await detectComments(pendingCall.filePath, content)
|
||||||
|
const filteredComments = applyFilters(rawComments)
|
||||||
|
|
||||||
|
if (filteredComments.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileComments: FileComments[] = [
|
||||||
|
{
|
||||||
|
filePath: pendingCall.filePath,
|
||||||
|
comments: filteredComments,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const message = formatHookMessage(fileComments)
|
||||||
|
output.output += `\n\n${message}`
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from "./types"
|
||||||
|
export * from "./constants"
|
||||||
|
export * from "./detector"
|
||||||
|
export * from "./filters"
|
||||||
|
export * from "./output"
|
||||||
11
src/hooks/comment-checker/output/formatter.ts
Normal file
11
src/hooks/comment-checker/output/formatter.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { FileComments } from "../types"
|
||||||
|
import { HOOK_MESSAGE_HEADER } from "../constants"
|
||||||
|
import { buildCommentsXml } from "./xml-builder"
|
||||||
|
|
||||||
|
export function formatHookMessage(fileCommentsList: FileComments[]): string {
|
||||||
|
if (fileCommentsList.length === 0) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
const xml = buildCommentsXml(fileCommentsList)
|
||||||
|
return `${HOOK_MESSAGE_HEADER}${xml}\n`
|
||||||
|
}
|
||||||
2
src/hooks/comment-checker/output/index.ts
Normal file
2
src/hooks/comment-checker/output/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { buildCommentsXml } from "./xml-builder"
|
||||||
|
export { formatHookMessage } from "./formatter"
|
||||||
24
src/hooks/comment-checker/output/xml-builder.ts
Normal file
24
src/hooks/comment-checker/output/xml-builder.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { FileComments } from "../types"
|
||||||
|
|
||||||
|
function escapeXml(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCommentsXml(fileCommentsList: FileComments[]): string {
|
||||||
|
const lines: string[] = []
|
||||||
|
|
||||||
|
for (const fc of fileCommentsList) {
|
||||||
|
lines.push(`<comments file="${escapeXml(fc.filePath)}">`)
|
||||||
|
for (const comment of fc.comments) {
|
||||||
|
lines.push(`\t<comment line-number="${comment.lineNumber}">${escapeXml(comment.text)}</comment>`)
|
||||||
|
}
|
||||||
|
lines.push(`</comments>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n")
|
||||||
|
}
|
||||||
36
src/hooks/comment-checker/types.ts
Normal file
36
src/hooks/comment-checker/types.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export type CommentType = "line" | "block" | "docstring"
|
||||||
|
|
||||||
|
export interface CommentInfo {
|
||||||
|
text: string
|
||||||
|
lineNumber: number
|
||||||
|
filePath: string
|
||||||
|
commentType: CommentType
|
||||||
|
isDocstring: boolean
|
||||||
|
metadata?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LanguageConfig {
|
||||||
|
extensions: string[]
|
||||||
|
commentQuery: string
|
||||||
|
docstringQuery?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PendingCall {
|
||||||
|
filePath: string
|
||||||
|
content?: string
|
||||||
|
tool: "write" | "edit" | "multiedit"
|
||||||
|
sessionID: string
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileComments {
|
||||||
|
filePath: string
|
||||||
|
comments: CommentInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterResult {
|
||||||
|
shouldSkip: boolean
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommentFilter = (comment: CommentInfo) => FilterResult
|
||||||
@@ -2,3 +2,4 @@ export * from "./todo-continuation-enforcer"
|
|||||||
export * from "./context-window-monitor"
|
export * from "./context-window-monitor"
|
||||||
export * from "./session-notification"
|
export * from "./session-notification"
|
||||||
export * from "./session-recovery"
|
export * from "./session-recovery"
|
||||||
|
export * from "./comment-checker"
|
||||||
|
|||||||
Reference in New Issue
Block a user