From 6e68a11bebec9d1887f6a7e205c72a1055ad9b66 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Mon, 19 Oct 2026 21:01:42 +0000 Subject: [PATCH] =?UTF-8?q?fix(go):=20a=20constant=20in=20a=20const=20(=20?= =?UTF-8?q?=E2=80=A6=20)=20group=20keeps=20its=20own=20doc=20comment=20(#2?= =?UTF-8?q?680)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both extractors read every member's docstring from the comment above `const (`, which documents the group. A constant documented inside the group, like gin's ErrorTypeBind under "ErrorTypeBind is used when Context.Bind() fails.", stored no docstring, or the group's comment instead of its own. A member's own doc comment, read with Go's doc comment rules, is now its docstring. A member without one keeps the group's comment, as before. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/fixtures/kernel-parity/torture.go | 7 + __tests__/go-grouped-const-docs.test.ts | 157 ++++++++++++++++++++ codegraph-kernel/src/go.rs | 6 +- src/extraction/tree-sitter.ts | 4 +- 5 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 __tests__/go-grouped-const-docs.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a9d902424..59fdc88f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Go, a type declared on its own, like `type Point struct{…}` under a `// Point is …` comment, now keeps that comment as its documentation, as a type inside a `type ( … )` group already did, so searching for what a type does can find it. A comment at the end of a line of code, like `const sides = 4 // sides of a square.`, is no longer taken as the documentation of the declaration under it. Re-index Go projects after upgrading. - In Go, a declaration's documentation is now just the comment written right above it, as `go doc` shows it. A section banner, a TODO or commented-out code that a blank line separates from a function, type, constant or variable is no longer added to the front of its documentation, or taken as its documentation when it has none of its own. Re-index Go projects after upgrading. - In Go, a comment directive written above a declaration, like `//go:embed static`, `//go:generate stringer -type=Kind` or `//nolint:errcheck`, is no longer part of its documentation, as `go doc` shows it, so a variable whose only comment is a `//go:embed` line no longer has that line as its description. Re-index Go projects after upgrading. +- In Go, a constant declared inside a `const ( … )` group now keeps the doc comment written above it, like gin's `// ErrorTypeBind is used when Context.Bind() fails.`, so searching for what a constant means can find it. Before, every member of the group got the group's comment, or nothing when the group had none; a member without a comment of its own still gets the group's. Re-index Go projects after upgrading. - Indexing a project that includes large bundled JavaScript files, such as a copy of pdf.js or d3, is fast again: since 1.6.2, resolving the calls in a JavaScript or TypeScript file re-read the file's text above each call, so a single bundled library could add many seconds to an index. The graph it builds is unchanged. Thanks @bompus for the report. (#2334) - Indexing a project that ships a large minified JavaScript file, such as the GraphiQL bundle inside go-ethereum, no longer stalls at "Resolving refs" when CodeGraph runs on Node 22, and linking such a file is faster on any Node version. The graph it builds is unchanged. - The time `codegraph init` and `codegraph index` print beside the node and edge counts now covers the whole run, resolving references and linking included, and `codegraph sync` reports its whole run the same way. Before, it counted only reading and parsing the files, which can be a small part of an index, so a slow index looked fast. Thanks @bompus for the report. (#2334) diff --git a/__tests__/fixtures/kernel-parity/torture.go b/__tests__/fixtures/kernel-parity/torture.go index 115a1bdfc..e227ee457 100644 --- a/__tests__/fixtures/kernel-parity/torture.go +++ b/__tests__/fixtures/kernel-parity/torture.go @@ -8,6 +8,13 @@ import ( const MAX_ITEMS = 128 // the line's own comment, not DefaultRegistry's doc +// Limits: the group's comment, for a member without its own. +const ( + // MinItems is documented inside its group. + MinItems = 1 + MaxSlots = 64 +) + var DefaultRegistry = NewRegistry() var handlerTable = map[string]func(int){ diff --git a/__tests__/go-grouped-const-docs.test.ts b/__tests__/go-grouped-const-docs.test.ts new file mode 100644 index 000000000..383e44f47 --- /dev/null +++ b/__tests__/go-grouped-const-docs.test.ts @@ -0,0 +1,157 @@ +/** + * A constant declared inside a `const ( … )` group lost the doc comment + * written above it: + * + * const ( + * // ErrorTypeBind is used when Context.Bind() fails. + * ErrorTypeBind ErrorType = 1 << 63 + * + * gin's `ErrorTypeBind` had no docstring, because both extractors read every + * member's docstring from the comment above `const (`, the group's. A member's + * own doc comment, the one go doc prints beside it, is now its docstring; a + * member without one keeps the group's comment, as before. + * + * Runs against the native kernel (when built) and the wasm extractor, which + * must agree. + */ +import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import { extractFromSource } from '../src/extraction'; +import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; +import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel'; +import type { ExtractionResult } from '../src/types'; + +const KERNEL_PATH = path.join( + __dirname, + '..', + 'codegraph-kernel', + 'prebuilds', + `${process.platform}-${process.arch}`, + 'codegraph-kernel.node' +); +const kernelAvailable = fs.existsSync(KERNEL_PATH) || process.env.CODEGRAPH_KERNEL_EXPECT === '1'; + +const SOURCE = `// Package shapes is documented. +package shapes + +// Kinds of shapes. The comment documents the group. +const ( + // Circle is documented on its own. + Circle = iota + Square + Pentagon = 5 // five sides + Heptagon + // A note a blank line separates from Hexagon. + + Hexagon + // Octagon is documented on its own. + //nolint:revive + Octagon +) + +const ( + // Red is documented, and its group is not. + Red = "red" + Blue = "blue" +) + +// Answer is documented above its own declaration. +const Answer = 42 + +// Lone is the only member of its group. +const ( + Lone = 1 +) + +// Own's group comment. +const ( + // Own is documented inside its group of one. + Own = 2 +) + +// limit is documented above its own declaration. +var limit = 10 +`; + +const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const; + +describe('Go constants in a group keep their own doc comment', () => { + let savedEnv: Record = {}; + + beforeAll(async () => { + await initGrammars(); + await loadGrammarsForLanguages(['go']); + }); + + beforeEach(() => { + savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]])); + resetKernelForTests(); + }); + + afterEach(() => { + for (const k of ENV_KEYS) { + if (savedEnv[k] === undefined) delete process.env[k]; + else process.env[k] = savedEnv[k]; + } + resetKernelForTests(); + }); + + function extract(backend: 'kernel' | 'wasm', source: string): ExtractionResult { + if (backend === 'wasm') { + process.env.CODEGRAPH_KERNEL = '0'; + return extractFromSource('shapes/shapes.go', source, 'go'); + } + delete process.env.CODEGRAPH_KERNEL; + process.env.CODEGRAPH_KERNEL_LANGS = 'all'; + const result = tryKernelExtract('shapes/shapes.go', source, 'go'); + expect(result, 'kernel extraction').not.toBeNull(); + return result!; + } + + const backends = kernelAvailable ? (['kernel', 'wasm'] as const) : (['wasm'] as const); + + for (const crlf of [false, true]) { + it.each(backends)(`%s${crlf ? ' (CRLF)' : ''}`, (backend) => { + const result = extract(backend, crlf ? SOURCE.replace(/\n/g, '\r\n') : SOURCE); + const doc = (kind: string, name: string): string | null => { + const found = result.nodes.filter((n) => n.kind === kind && n.name === name); + expect(found, `${kind} ${name}`).toHaveLength(1); + return found[0].docstring ?? null; + }; + const group = 'Kinds of shapes. The comment documents the group.'; + + expect({ + // A member's own doc comment is its docstring, read the way go doc + // reads one: without a directive, and ended by a blank line. + Circle: doc('constant', 'Circle'), + Octagon: doc('constant', 'Octagon'), + Red: doc('constant', 'Red'), + Own: doc('constant', 'Own'), + // A member without one keeps the group's comment, if there is one; a + // comment after the code on the line above, or one a blank line + // separates from the member, is not its doc. + Square: doc('constant', 'Square'), + Heptagon: doc('constant', 'Heptagon'), + Hexagon: doc('constant', 'Hexagon'), + Blue: doc('constant', 'Blue'), + Lone: doc('constant', 'Lone'), + // A declaration on its own is read as before. + Answer: doc('constant', 'Answer'), + limit: doc('variable', 'limit'), + }).toEqual({ + Circle: 'Circle is documented on its own.', + Octagon: 'Octagon is documented on its own.', + Red: 'Red is documented, and its group is not.', + Own: 'Own is documented inside its group of one.', + Square: group, + Heptagon: group, + Hexagon: group, + Blue: null, + Lone: 'Lone is the only member of its group.', + Answer: 'Answer is documented above its own declaration.', + limit: 'limit is documented above its own declaration.', + }); + }); + } +}); diff --git a/codegraph-kernel/src/go.rs b/codegraph-kernel/src/go.rs index b0e0d1cce..d27198c6c 100644 --- a/codegraph-kernel/src/go.rs +++ b/codegraph-kernel/src/go.rs @@ -697,11 +697,15 @@ impl<'t> Walker<'t> { None }; let signature = value_node.map(|v| util::init_signature(self.text(v))); + // A member of a `const ( … )` group has its own doc + // comment beside it in the parentheses; one without keeps + // the group's. + let doc = self.docstring_of(spec).or_else(|| docstring.clone()); var_row = self.create_node( if is_const_decl { "constant" } else { "variable" }, &name, spec, - Extra { docstring: docstring.clone(), signature, ..Extra::default() }, + Extra { docstring: doc, signature, ..Extra::default() }, ); } } diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 61abda9a7..2b7f0dfdb 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -3572,8 +3572,10 @@ export class TreeSitterExtractor { const initValue = valueNode ? getNodeText(valueNode, this.source).slice(0, 100) : undefined; const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined; + // A member of a `const ( … )` group has its own doc comment beside + // it in the parentheses; one without keeps the group's. varNode = this.createNode(node.type === 'const_declaration' ? 'constant' : 'variable', name, spec, { - docstring, + docstring: this.docstringFor(spec) ?? docstring, signature: initSignature, }); }