Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Go, a function or method passed as a value, like `sync.Pool{New: wm.new}`, now shows the function that passes it among its callers and impact even when it is named `new`, `nil`, `None`, `self` or a handful of similar words, or when a line break or comment sits beside the dot. Before, such a value was skipped, so a method used only that way looked unused. Re-index Go projects after upgrading.
- In Go, a type in a parameter, a result or a composite literal now always links to a type, never to a method or function that shares its name. Before, an unexported type like `keyIndex` in etcd's `func (ti *treeIndex) KeyIndex(keyi *keyIndex) *keyIndex` linked to the `keyIndex` method declared beside it, prometheus's `samples{…}` literals to a `samples` method, and a type from a package outside your project, like `config.URL` in `&config.URL{…}`, to whatever project method had that name. A type written without a package, including an unexported one or one named like `AppenderV2` or `Histogram_CountInt`, now links to its own package's type when there is one, and a generic method's type parameter, like `T` in `func (p *Pool[T]) Get() T`, no longer links to an unrelated `T`. Re-index Go projects after upgrading.
- 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.
- 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)
Expand Down
5 changes: 5 additions & 0 deletions __tests__/fixtures/kernel-parity/torture.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type Widget struct {
name string
}

// Containers: a banner a blank line separates from Stack, not its doc.

type Stack[T any] struct {
items []T
}
Expand Down Expand Up @@ -160,6 +162,9 @@ func (w Widget) Render() string {
return w.name
}

// Callbacks: a section comment above TargetCb's own doc.

// TargetCb is documented right above.
func TargetCb(n int) {}

func shadowed() {
Expand Down
208 changes: 208 additions & 0 deletions __tests__/go-detached-doc-comments.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/**
* A Go declaration's docstring took every comment written above it, including
* ones a blank line separates from it. In gin, `Context.Next` sits under a
* FLOW CONTROL banner of block comments, then a blank line, then its own doc
* comment, and its docstring began "/\nFLOW CONTROL\n/\n". A section banner, a
* TODO or commented-out code with nothing under it became the doc of whatever
* came next.
*
* Go's doc comment, the one go doc shows (go/parser's lead comment), is only
* the comment group that ends on the line right above the declaration:
* comments on consecutive lines form one group, and a blank line ends it. Go
* now reads its docstrings that way.
*
* 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 = `// Copyright 2026 The Shapes Authors.

// Package shapes is documented.
package shapes

/*** CONTENT NEGOTIATION ***/

// Negotiate is documented right above, under a banner.
func Negotiate() {}

// TODO: a note a blank line separates from the function below.

func Detached() {}

// ==== Shapes START ====

type Bare struct{}

// An orphaned copy of another declaration's doc.

// Point is documented right above.
type Point struct{ X, Y int }

// IsSafe is a hack, documented for something else.

type ReadTx interface{}

// Lengths.

// Len is documented right above a method.
func (p Point) Len() int { return 0 }

// interfaces for sorting

func (p Point) Less() bool { return false }

// Joined starts with a line comment,
/*
continues in a block comment
*/
// and ends right above.
func Joined() {}

/*
Box is documented in a block comment

with a blank line inside it.
*/
type Box struct{}

// Para is documented in two paragraphs.
//
// The second paragraph.
func Para() {}

/* Inline is written on the declaration's own line. */ func Inline() {}

// Limits.

// maxItems is documented right above.
const maxItems = 10

// A note about the registry.

var registry = map[string]int{}

type (
// Circle is documented a blank line above.

Circle struct{}
// Square is documented right above.
Square struct{}
)

var origin = Point{} // the origin.
// Polygon is documented right below a commented line.
type Polygon struct{}
`;

const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const;

describe('Go doc comments end at a blank line', () => {
let savedEnv: Record<string, string | undefined> = {};

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;
};
// A block comment keeps the line breaks written inside it.
const eol = (text: string) => (crlf ? text.replace(/\n/g, '\r\n') : text);

expect({
// A comment a blank line separates from the declaration is not its
// doc, whether or not a doc comment comes between them.
Negotiate: doc('function', 'Negotiate'),
Detached: doc('function', 'Detached'),
Bare: doc('struct', 'Bare'),
Point: doc('struct', 'Point'),
ReadTx: doc('interface', 'ReadTx'),
Len: doc('method', 'Len'),
Less: doc('method', 'Less'),
maxItems: doc('constant', 'maxItems'),
registry: doc('variable', 'registry'),
Circle: doc('struct', 'Circle'),
Square: doc('struct', 'Square'),
// Comments on consecutive lines are one doc comment, line and block
// comments alike, and a blank line inside a block comment or an empty
// `//` line does not end it.
Joined: doc('function', 'Joined'),
Box: doc('struct', 'Box'),
Para: doc('function', 'Para'),
// The doc comment ends on the line above the declaration, not on the
// declaration's own line.
Inline: doc('function', 'Inline'),
// A comment after code on its line is that line's, and the doc
// comment right below it starts after it.
Polygon: doc('struct', 'Polygon'),
}).toEqual({
Negotiate: 'Negotiate is documented right above, under a banner.',
Detached: null,
Bare: null,
Point: 'Point is documented right above.',
ReadTx: null,
Len: 'Len is documented right above a method.',
Less: null,
maxItems: 'maxItems is documented right above.',
registry: null,
Circle: null,
Square: 'Square is documented right above.',
Joined: 'Joined starts with a line comment,\ncontinues in a block comment\nand ends right above.',
Box: eol('Box is documented in a block comment\n\nwith a blank line inside it.'),
Para: 'Para is documented in two paragraphs.\n\nThe second paragraph.',
Inline: null,
Polygon: 'Polygon is documented right below a commented line.',
});
});
}
});
70 changes: 60 additions & 10 deletions codegraph-kernel/src/docstring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,23 +143,27 @@ pub fn preceding_docstring_stepping_over(
src: &str,
step_over: &[&str],
) -> Option<String> {
preceding_docstring_with(node, src, step_over, false)
preceding_docstring_with(node, src, step_over, false, false)
}

/// getPrecedingDocstring's `skipTrailing` (Go) — the comments the run opens
/// with that belong to the line above are left out: one written after code on
/// its line (`const n = 4 // four.`), and any that begins on the line such a
/// comment ends. Go reads them as that line's comment, never the next
/// declaration's doc.
pub fn preceding_docstring_skipping_trailing(node: Node, src: &str) -> Option<String> {
preceding_docstring_with(node, src, &[], true)
/// getPrecedingDocstring's `skipTrailing` and `skipDetached` (Go). The
/// comments the run opens with that belong to the line above are left out:
/// one written after code on its line (`const n = 4 // four.`), and any that
/// begins on the line such a comment ends. Of the rest, only the comments that
/// end on the line right above the declaration, together with the ones written
/// on the lines just before them, are kept: a blank line ends the docstring,
/// and a comment that ends on the declaration's own line documents nothing.
/// That is Go's doc comment, the one go doc shows.
pub fn preceding_docstring_skipping_trailing_and_detached(node: Node, src: &str) -> Option<String> {
preceding_docstring_with(node, src, &[], true, true)
}

fn preceding_docstring_with(
node: Node,
src: &str,
step_over: &[&str],
skip_trailing: bool,
skip_detached: bool,
) -> Option<String> {
let mut anchor = node;
while let Some(parent) = anchor.parent() {
Expand Down Expand Up @@ -200,6 +204,21 @@ fn preceding_docstring_with(
first += 1;
}
}
if skip_detached {
// Walk back from the declaration: the last comment must end on the
// line above it, and each one before joins while it ends on the line
// the next one starts, or the line before that.
let mut start = comments.len();
if start > first && comments[start - 1].end_position().row + 1 == anchor.start_position().row {
start -= 1;
while start > first
&& comments[start - 1].end_position().row + 1 >= comments[start].start_position().row
{
start -= 1;
}
}
first = start;
}
if first == comments.len() {
return None;
}
Expand Down Expand Up @@ -294,9 +313,40 @@ mod tests {
})
.expect(name)
};
let skipping_trailing = |n| preceding_docstring_with(n, src, &[], true, false);
assert_eq!(preceding_docstring(func("f"), src).as_deref(), Some("a\nb\nc"));
assert_eq!(preceding_docstring_skipping_trailing(func("f"), src).as_deref(), Some("c"));
assert_eq!(skipping_trailing(func("f")).as_deref(), Some("c"));
assert_eq!(preceding_docstring(func("g"), src).as_deref(), Some("d"));
assert_eq!(preceding_docstring_skipping_trailing(func("g"), src), None);
assert_eq!(skipping_trailing(func("g")), None);
}

/// Skipping detached comments keeps the comments that end on the line
/// above the declaration and those on the lines just before them; a blank
/// line ends the run, and a comment on the declaration's own line is not
/// kept. Without it, every comment is.
#[test]
fn skips_the_comments_a_blank_line_detaches() {
let grammar = crate::langs::grammar_for("go").expect("go grammar");
let mut parser = tree_sitter::Parser::new();
parser.set_language(&grammar).unwrap();
let src = "package p\n\n// a\n\n// b\n/*\nc\n*/\n// d\nfunc f() {}\n\n// e\n\nfunc g() {}\n\n/* h */ func h() {}\n";
let tree = parser.parse(src, None).unwrap();
let root = tree.root_node();
let func = |name: &str| {
(0..root.named_child_count())
.filter_map(|i| root.named_child(i))
.find(|n| {
n.kind() == "function_declaration"
&& n.child_by_field_name("name").map(|id| &src[id.byte_range()]) == Some(name)
})
.expect(name)
};
let skipping_detached = |n| preceding_docstring_with(n, src, &[], false, true);
assert_eq!(preceding_docstring(func("f"), src).as_deref(), Some("a\nb\nc\nd"));
assert_eq!(skipping_detached(func("f")).as_deref(), Some("b\nc\nd"));
assert_eq!(preceding_docstring(func("g"), src).as_deref(), Some("e"));
assert_eq!(skipping_detached(func("g")), None);
assert_eq!(preceding_docstring(func("h"), src).as_deref(), Some("h"));
assert_eq!(skipping_detached(func("h")), None);
}
}
11 changes: 8 additions & 3 deletions codegraph-kernel/src/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::buffers::{
build_meta, edge_kind_index, node_kind_index, Arena, BoolFlags, EdgeRow, EmitOut, NodeRow,
RefRow, StrRef, Tables, FLAG_IS_EXPORTED, FUNCTION_REF_CODE, NONE, NONE_STR,
};
use crate::docstring::preceding_docstring_skipping_trailing;
use crate::docstring::preceding_docstring_skipping_trailing_and_detached;
use crate::ids;
use crate::textutil as util;
use regex::Regex;
Expand Down Expand Up @@ -400,9 +400,14 @@ impl<'t> Walker<'t> {

/// docstringFor (tree-sitter.ts) — the preceding comment run, looked up
/// from the declaration wrapper when there is one, without the comments
/// that trail the code above it (docstringSkipsTrailingComments).
/// that trail the code above it (docstringSkipsTrailingComments) or that
/// don't end on the line right above it, past a blank line
/// (docstringSkipsDetachedComments).
fn docstring_of(&self, node: Node<'t>) -> Option<String> {
preceding_docstring_skipping_trailing(self.declaration_wrapper(node).unwrap_or(node), self.src)
preceding_docstring_skipping_trailing_and_detached(
self.declaration_wrapper(node).unwrap_or(node),
self.src,
)
}

// --- visitNode ------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions src/extraction/languages/go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,8 @@ export const goExtractor: LanguageExtractor = {
// A comment after code on its line (`const sides = 4 // sides of a square.`)
// is that line's, never the doc of the declaration below it.
docstringSkipsTrailingComments: true,
// A doc comment ends on the line right above its declaration, and a blank
// line ends it, as go doc reads it: a banner or a note above that blank line
// documents nothing.
docstringSkipsDetachedComments: true,
};
Loading