Skip to content

8.3. Jump to definition#

Another often used feature of an IDE is the ability to jump to the definition of a given identifier. Again, we can use the Binding Graph API to do it:

jump-to-definition.mts
import { assertTerminalNode, TerminalKindExtensions } from "@nomicfoundation/slang/cst";
import { CompilationUnit } from "@nomicfoundation/slang/compilation";
import { findTerminalNodeAt } from "../../common/find-terminal-node-at.mjs";

type Target = {
  file: string;
  line: number;
  column: number;
};

export function jumpToDefinition(unit: CompilationUnit, fileId: string, line: number, column: number): Target {
  const file = unit.file(fileId);
  if (!file) {
    throw new Error(`${fileId} not found in compilation unit`);
  }

  const cursor = findTerminalNodeAt(file.createTreeCursor(), line, column);
  if (!cursor) {
    throw new Error(`${fileId}:${line}:${column} is not a valid text location`);
  }

  assertTerminalNode(cursor.node);
  if (!TerminalKindExtensions.isIdentifier(cursor.node.kind)) {
    // location is not a valid identifier
    throw new Error(`Could not find a valid identifier at ${fileId}:${line}:${column}`);
  }

  const reference = unit.bindingGraph.referenceAt(cursor);
  if (!reference) {
    throw new Error(`Identifier ${cursor.node.unparse()} is not a reference at ${fileId}:${line}:${column}`);
  }

  const definitions = reference.definitions();
  if (definitions.length == 0) {
    throw new Error(`${cursor.node.unparse()} is not defined`);
  }

  // we take the first definition arbitrarily
  const location = definitions[0].nameLocation;
  if (!location.isUserFileLocation()) {
    throw new Error(`${cursor.node.unparse()} is a built-in`);
  }

  return {
    file: location.fileId,
    line: location.cursor.textOffset.line,
    column: location.cursor.textOffset.column,
  };
}

The following example shows jumping to the definition of the parameter delta in line 11:

contract Counter {
  uint _count;
  constructor(uint initialCount) {
    _count = initialCount;
  }
  function count() public view returns (uint) {
    return _count;
  }
  function increment(uint delta) public returns (uint) {
    require(delta > 0, "Delta must be positive");
    _count += delta;
    return _count;
  }
}
test-jump-to-definition.mts
import assert from "node:assert";
import { jumpToDefinition } from "./jump-to-definition.mjs";
import buildSampleCompilationUnit from "../../common/sample-contract.mjs";

test("jump to definition", async () => {
  const unit = await buildSampleCompilationUnit();

  // the reference to `delta` in the assignment addition
  const definition = jumpToDefinition(unit, "contract.sol", 11, 16);

  assert.deepEqual(definition, { file: "contract.sol", line: 9, column: 26 });
});