This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit fc7533165d0b2d9395e89533318513d895e7f1a3
Author: Stephen Mallette <[email protected]>
AuthorDate: Fri Oct 9 15:44:18 2026 -0400

    Let tinker-review code refs name fields
    
    Field declarations are indexed, so a ref such as Type.field links to the 
field.
    
    Assisted-by: Claude Code:claude-opus-5-5
---
 .skills/tinker-review/SKILL.md                     |  7 +--
 .../tinker-review/scripts/extraction/code-index.js |  9 ++--
 .../scripts/extraction/tree-sitter.js              | 51 +++++++++++++++++++++-
 .../tinker-review/scripts/renderer/code-links.js   | 11 +++--
 .skills/tinker-review/test/code-links.test.js      | 16 +++++++
 .skills/tinker-review/test/extraction.test.js      | 15 +++++++
 6 files changed, 96 insertions(+), 13 deletions(-)

diff --git a/.skills/tinker-review/SKILL.md b/.skills/tinker-review/SKILL.md
index 87e65c27ce..05556edbeb 100644
--- a/.skills/tinker-review/SKILL.md
+++ b/.skills/tinker-review/SKILL.md
@@ -267,9 +267,10 @@ produce a complete evidence-with-narrative JSON file. 
Write it to
     reasonable?"*) and may cite what you found (a finding, a functional-test 
label). Invite
     judgment of trade-offs rather than asserting them.
   - `refs` on a question — the code that answers it, as `{ file, symbol?, 
lines?, label? }`:
-    `file` is a repo path or unique suffix (`"TinkerTransaction.java"`); 
`symbol` a function
-    or type name, or `"Type.member"` for a member 
(`"TinkerStorageGraph.TinkerStorageGraph"`
-    is the constructor; bare `"TinkerStorageGraph"` the class); `label` the 
chip text. Name
+    `file` is a repo path or unique suffix (`"TinkerTransaction.java"`); 
`symbol` a function,
+    type or field name, or `"Type.member"` for a method or field of that type
+    (`"TinkerStorageGraph.TinkerStorageGraph"` is the constructor; bare 
`"TinkerStorageGraph"`
+    the class); `label` the chip text. Name
     the code — **never write line numbers or URLs**: the renderer resolves 
each ref against
     `evidence.codeIndex` and links to the file pinned at the reviewed commit 
with the
     symbol's lines highlighted. Two or three
diff --git a/.skills/tinker-review/scripts/extraction/code-index.js 
b/.skills/tinker-review/scripts/extraction/code-index.js
index 7e330490d2..fd1d296598 100644
--- a/.skills/tinker-review/scripts/extraction/code-index.js
+++ b/.skills/tinker-review/scripts/extraction/code-index.js
@@ -19,8 +19,8 @@
 
 /**
  * @typedef {object} CodeSymbol
- * @property {string} name - function or type name as extracted
- * @property {"function"|"type"} kind
+ * @property {string} name - function, type or field name as extracted
+ * @property {"function"|"type"|"field"} kind
  * @property {number} start - first line (1-based, PR head side)
  * @property {number} end - last line (inclusive)
  */
@@ -71,12 +71,12 @@ export function parseHunks(diffText) {
 /**
  * Build the code index the renderer uses to turn a `{file, symbol}` reference 
into
  * a link: for every changed file, its changed hunks and the line range of each
- * function and type extracted from it. Files outside `changedFiles` (such as 
the
+ * function, type and field extracted from it. Files outside `changedFiles` 
(such as the
  * hierarchy neighborhood parsed for context) are left out.
  *
  * @param {object} params
  * @param {string[]} params.changedFiles
- * @param {{functions: object[], types: object[]}} params.extraction
+ * @param {{functions: object[], types: object[], fields?: object[]}} 
params.extraction
  * @param {string} params.diffText - `git diff --unified=0` of the PR against 
its merge base
  * @returns {CodeIndex}
  */
@@ -94,6 +94,7 @@ export function buildCodeIndex({ changedFiles, extraction, 
diffText }) {
   };
   for (const t of extraction.types || []) add(t, "type");
   for (const f of extraction.functions || []) add(f, "function");
+  for (const f of extraction.fields || []) add(f, "field");
   for (const entry of Object.values(files)) entry.symbols.sort((a, b) => 
a.start - b.start);
   return { files };
 }
diff --git a/.skills/tinker-review/scripts/extraction/tree-sitter.js 
b/.skills/tinker-review/scripts/extraction/tree-sitter.js
index fddd85cc60..24a7247281 100644
--- a/.skills/tinker-review/scripts/extraction/tree-sitter.js
+++ b/.skills/tinker-review/scripts/extraction/tree-sitter.js
@@ -492,6 +492,51 @@ function extractTypesFromTree(tree, filePath, language) {
   return types;
 }
 
+// Field declarations, so a report ref can name a field (`Type.field`). They 
feed
+// only the code index; the graph has no Field vertex. A declaration such as
+// `int a, b;` yields one entry per name, each spanning the whole declaration.
+const FIELD_NODES = {
+  java: "field_declaration",
+  csharp: "field_declaration",
+  go: "field_declaration",
+  javascript: "field_definition",
+};
+
+function fieldNames(node) {
+  // Java puts the declarators directly under the declaration, C# one level 
down.
+  const holder = childByType(node, "variable_declaration") || node;
+  const declarators = childrenByType(holder, "variable_declarator");
+  if (declarators.length > 0) {
+    return declarators
+      .map((d) => (d.childForFieldName("name") || childByType(d, 
"identifier"))?.text)
+      .filter(Boolean);
+  }
+  // Go struct fields carry field_identifiers; JS class fields a property name.
+  const names = childrenByType(node, "field_identifier").map((n) => n.text);
+  const prop = node.childForFieldName("property") || childByType(node, 
"property_identifier");
+  if (prop) names.push(prop.text);
+  return names;
+}
+
+function extractFieldsFromTree(tree, filePath, language) {
+  const nodeType = FIELD_NODES[language];
+  if (!nodeType) return [];
+  const fields = [];
+  (function visit(node) {
+    if (node.type === nodeType) {
+      for (const name of fieldNames(node)) {
+        fields.push({
+          name, filePath, language,
+          linesStart: node.startPosition.row + 1,
+          linesEnd: node.endPosition.row + 1,
+        });
+      }
+    }
+    for (let i = 0; i < node.childCount; i++) visit(node.child(i));
+  })(tree.rootNode);
+  return fields;
+}
+
 // Which type declares each function, for `declares` (Type -> Function) edges.
 // Walks the tree carrying the innermost enclosing type so a method maps to the
 // class/interface whose body it sits in (both keyed within the same file).
@@ -778,6 +823,7 @@ function parseSourceFile(parser, file, language, result, 
baseContents = {}) {
   result.functions.push(...fileFunctions);
   result.types.push(...fileTypes);
   result.declares.push(...extractDeclaresFromTree(tree, file.path, language));
+  result.fields.push(...extractFieldsFromTree(tree, file.path, language));
 
   const fileCalls = extractCallsFromTree(tree, file.path, language, 
fileFunctions);
   result.calls.push(...fileCalls);
@@ -944,6 +990,7 @@ export async function extract(directory, language, options 
= {}) {
     imports: [],
     tests: [],
     declares: [],
+    fields: [],
   };
 
   for (const file of sourceFiles) {
@@ -987,7 +1034,7 @@ export async function extract(directory, language, options 
= {}) {
 export async function extractMulti(directory, languages, options = {}) {
   const merged = {
     languages: [],
-    files: [], functions: [], types: [], calls: [], imports: [], tests: [], 
declares: [],
+    files: [], functions: [], types: [], calls: [], imports: [], tests: [], 
declares: [], fields: [],
     hierarchyNeighborhood: { files: 0, truncated: false },
   };
 
@@ -995,7 +1042,7 @@ export async function extractMulti(directory, languages, 
options = {}) {
     if (!LANGUAGE_EXTENSIONS[language]) continue;
     const one = await extract(directory, language, options);
     merged.languages.push(language);
-    for (const key of ["files", "functions", "types", "calls", "imports", 
"tests", "declares"]) {
+    for (const key of ["files", "functions", "types", "calls", "imports", 
"tests", "declares", "fields"]) {
       // Not push(...one[key]): spreading a large array exceeds the call stack.
       for (const item of one[key]) merged[key].push(item);
     }
diff --git a/.skills/tinker-review/scripts/renderer/code-links.js 
b/.skills/tinker-review/scripts/renderer/code-links.js
index 2a1e9001f3..6c7519fb19 100644
--- a/.skills/tinker-review/scripts/renderer/code-links.js
+++ b/.skills/tinker-review/scripts/renderer/code-links.js
@@ -28,7 +28,7 @@ const REPO = "https://github.com/apache/tinkerpop";;
  *
  * @typedef {object} CodeRef
  * @property {string} file - repo-relative path, or a unique suffix of one 
(e.g. "TinkerTransaction.java")
- * @property {string} [symbol] - function or type name; "Type.member" picks a 
member of that type
+ * @property {string} [symbol] - function, type or field name; "Type.member" 
picks a method or field of that type
  *   (so "TinkerStorageGraph.TinkerStorageGraph" is the constructor, bare 
"TinkerStorageGraph" the class)
  * @property {[number, number]} [lines] - explicit line range on the PR head 
side, used instead of symbol
  * @property {string} [label] - chip text; defaults to the symbol, else the 
file name
@@ -64,14 +64,17 @@ function findSymbol(entry, symbol) {
     const member = symbol.slice(dot + 1);
     const types = entry.symbols.filter((s) => s.kind === "type" && s.name === 
owner);
     for (const t of types) {
-      const fn = entry.symbols.find((s) => s.kind === "function" && s.name === 
member && s.start >= t.start && s.end <= t.end);
-      if (fn) return fn;
+      const inside = (s) => s.name === member && s.start >= t.start && s.end 
<= t.end;
+      const found = entry.symbols.find((s) => s.kind === "function" && 
inside(s))
+        || entry.symbols.find((s) => s.kind === "field" && inside(s));
+      if (found) return found;
     }
     return null;
   }
-  // a bare name means the type when one exists (a constructor shares its 
class's name), else the first function
+  // a bare name means the type when one exists (a constructor shares its 
class's name), else the first function, else a field
   return entry.symbols.find((s) => s.kind === "type" && s.name === symbol)
     || entry.symbols.find((s) => s.kind === "function" && s.name === symbol)
+    || entry.symbols.find((s) => s.kind === "field" && s.name === symbol)
     || null;
 }
 
diff --git a/.skills/tinker-review/test/code-links.test.js 
b/.skills/tinker-review/test/code-links.test.js
index 6993790562..05272383d7 100644
--- a/.skills/tinker-review/test/code-links.test.js
+++ b/.skills/tinker-review/test/code-links.test.js
@@ -159,3 +159,19 @@ test("the guided walk renders questions with chips and 
reports unresolved refs",
   assert.equal(warnings.length, 1);
   assert.match(warnings[0], /guidedWalk\[0\]\.questions\[0\]: unresolved: no 
symbol "ghost"/);
 });
+
+test("a Type.field ref resolves to the field declaration", () => {
+  const index = buildCodeIndex({
+    changedFiles: [TX],
+    extraction: {
+      types: [{ name: "TinkerTransaction", filePath: TX, linesStart: 30, 
linesEnd: 400 }],
+      functions: [],
+      fields: [{ name: "storageCommitLock", filePath: TX, linesStart: 42, 
linesEnd: 42 }],
+    },
+    diffText: DIFF,
+  });
+  const ctx = { pr: 3639, headSha: "abc123", codeIndex: index };
+  const r = resolveRef({ file: TX, symbol: 
"TinkerTransaction.storageCommitLock" }, ctx);
+  assert.match(r.url, /#L42-L42$/);
+  assert.match(resolveRef({ file: TX, symbol: "storageCommitLock" }, ctx).url, 
/#L42-L42$/, "bare field name");
+});
diff --git a/.skills/tinker-review/test/extraction.test.js 
b/.skills/tinker-review/test/extraction.test.js
index cde766ba73..ca8c1754aa 100644
--- a/.skills/tinker-review/test/extraction.test.js
+++ b/.skills/tinker-review/test/extraction.test.js
@@ -171,3 +171,18 @@ test("expandHierarchy:false keeps extraction to changed 
files only", async () =>
     await rm(dir, { recursive: true, force: true });
   }
 });
+
+test("Java fields are extracted per name, spanning their declaration", async 
() => {
+  await withJavaSources(
+    {
+      "Graph.java":
+        "package x;\npublic class Graph {\n  private final Object 
storageCommitLock = new Object();\n  int a,\n      b;\n  void run() { int local 
= 1; }\n}\n",
+    },
+    (r) => {
+      const byName = Object.fromEntries(r.fields.map((f) => [f.name, f]));
+      assert.deepEqual(Object.keys(byName).sort(), ["a", "b", 
"storageCommitLock"], "locals are not fields");
+      assert.deepEqual([byName.storageCommitLock.linesStart, 
byName.storageCommitLock.linesEnd], [3, 3]);
+      assert.deepEqual([byName.b.linesStart, byName.b.linesEnd], [4, 5]);
+    },
+  );
+});

Reply via email to