gdharness 1.1.61 → 1.1.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -9270,17 +9270,23 @@ function parametersOf(text, from) {
9270
9270
  }
9271
9271
  return names;
9272
9272
  }
9273
- function isLocalAt(lines, index, name) {
9273
+ function declaredClass(text, name) {
9274
+ const word = `(?<![\\p{L}\\p{N}_.])${escapedForPattern(name)}`;
9275
+ const found = new RegExp(`${word}\\s*:\\s*(${DOTTED})`, "u").exec(text)?.[1] ?? new RegExp(`${word}\\s*:=\\s*(${DOTTED})\\.new\\s*\\(`, "u").exec(text)?.[1] ?? new RegExp(`${word}\\s*:?=[^\\n]*?\\bas\\s+(${DOTTED})\\s*$`, "u").exec(text)?.[1];
9276
+ return found === undefined ? null : found.slice(found.lastIndexOf(".") + 1);
9277
+ }
9278
+ function localAt(lines, index, name) {
9274
9279
  const word = escapedForPattern(name);
9275
9280
  const declares = new RegExp(`(?<![\\p{L}\\p{N}_.])(?:var|const)\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
9276
9281
  const iterates = new RegExp(`^\\s*for\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
9282
+ const local = (text) => ({ type: declaredClass(text, name) });
9277
9283
  const own = lines[index] ?? "";
9278
9284
  const headerOnOwnLine = FUNC_LINE.exec(own) ?? LAMBDA_HEADER.exec(own);
9279
9285
  if (headerOnOwnLine !== null && parametersOf(own, headerOnOwnLine.index).includes(name)) {
9280
- return true;
9286
+ return local(own);
9281
9287
  }
9282
9288
  if (FUNC_LINE.test(own)) {
9283
- return false;
9289
+ return null;
9284
9290
  }
9285
9291
  let depth = indentOf(own);
9286
9292
  for (let at = index - 1;at >= 0; at -= 1) {
@@ -9296,21 +9302,21 @@ function isLocalAt(lines, index, name) {
9296
9302
  if (indent < depth) {
9297
9303
  depth = indent;
9298
9304
  if (named !== null) {
9299
- return parametersOf(text, named.index).includes(name);
9305
+ return parametersOf(text, named.index).includes(name) ? local(text) : null;
9300
9306
  }
9301
9307
  const lambda = LAMBDA_HEADER.exec(text);
9302
9308
  if (lambda !== null && parametersOf(text, lambda.index).includes(name) || iterates.test(text)) {
9303
- return true;
9309
+ return local(text);
9304
9310
  }
9305
9311
  }
9306
9312
  if (named !== null || depth === 0) {
9307
- return false;
9313
+ return null;
9308
9314
  }
9309
9315
  if (declares.test(text)) {
9310
- return true;
9316
+ return local(text);
9311
9317
  }
9312
9318
  }
9313
- return false;
9319
+ return null;
9314
9320
  }
9315
9321
  function receiverBefore(code, offset) {
9316
9322
  let at = offset - 1;
@@ -9328,7 +9334,14 @@ function receiverBefore(code, offset) {
9328
9334
  while (start > 0 && isIdentifierPart(code[start - 1])) {
9329
9335
  start -= 1;
9330
9336
  }
9331
- return start <= at ? code.slice(start, at + 1) : null;
9337
+ if (start > at) {
9338
+ return null;
9339
+ }
9340
+ let before = start - 1;
9341
+ while (before >= 0 && /\s/.test(code[before] ?? "")) {
9342
+ before -= 1;
9343
+ }
9344
+ return { name: code.slice(start, at + 1), dotted: code[before] === "." };
9332
9345
  }
9333
9346
  function bodyHolding(body, offset) {
9334
9347
  const inner = body.inner.find((one) => one.start <= offset && offset <= one.end);
@@ -9341,13 +9354,27 @@ function callSitesOf(source, name, otherClasses = new Set) {
9341
9354
  `);
9342
9355
  const lines = new Lines(source);
9343
9356
  const shape = shapeOf(source);
9357
+ const memberClass = (member, at) => {
9358
+ const declared = bodyHolding(shape.body, at).declarations.find((one) => one.name === member && (one.kind === "var" || one.kind === "const"));
9359
+ return declared === undefined ? null : declaredClass(codeLines[lines.positionOf(declared.offset).line] ?? "", member);
9360
+ };
9344
9361
  return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).filter((one) => {
9362
+ const index = lines.positionOf(one.offset).line;
9345
9363
  if (one.afterDot) {
9346
9364
  const receiver = receiverBefore(code, one.offset);
9347
- return receiver === null || !otherClasses.has(receiver);
9365
+ if (receiver === null || otherClasses.size === 0) {
9366
+ return true;
9367
+ }
9368
+ if (otherClasses.has(receiver.name)) {
9369
+ return false;
9370
+ }
9371
+ if (receiver.dotted) {
9372
+ return true;
9373
+ }
9374
+ const typed = localAt(codeLines, index, receiver.name)?.type ?? memberClass(receiver.name, one.offset);
9375
+ return typed === null || !otherClasses.has(typed);
9348
9376
  }
9349
- const index = lines.positionOf(one.offset).line;
9350
- if (isLocalAt(codeLines, index, name)) {
9377
+ if (localAt(codeLines, index, name) !== null) {
9351
9378
  return false;
9352
9379
  }
9353
9380
  return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind));
@@ -9372,7 +9399,7 @@ function collectEnumValues(text, from, into) {
9372
9399
  function isIdentifier(name) {
9373
9400
  return new RegExp(`^${NAME}$`, "u").test(name) && !KEYWORDS.has(name);
9374
9401
  }
9375
- var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, FUNC_LINE, SUPER_CALL, LAMBDA_HEADER, MEMBER_KINDS, KEYWORDS;
9402
+ var IDENTIFIER_START, IDENTIFIER_PART, DECLARING, NAME = "[\\p{L}_][\\p{L}\\p{N}_]*", ANNOTATIONS = "(?:@[\\p{L}_][\\p{L}\\p{N}_]*(?:\\([^)]*\\))?\\s+)*", DECLARATION_LINE, CLASS_NAME_LINE, EXTENDS_LINE, EXTENDS_CLAUSE, FUNC_LINE, SUPER_CALL, LAMBDA_HEADER, DOTTED, MEMBER_KINDS, KEYWORDS;
9376
9403
  var init_gdscript_source = __esm(() => {
9377
9404
  IDENTIFIER_START = /[\p{L}_]/u;
9378
9405
  IDENTIFIER_PART = /[\p{L}\p{N}_]/u;
@@ -9384,6 +9411,7 @@ var init_gdscript_source = __esm(() => {
9384
9411
  FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
9385
9412
  SUPER_CALL = /(?<![\p{L}\p{N}_.])super\s*[.(]/u;
9386
9413
  LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
9414
+ DOTTED = `${NAME}(?:\\.${NAME})*`;
9387
9415
  MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
9388
9416
  KEYWORDS = new Set([
9389
9417
  "and",
@@ -18349,7 +18377,7 @@ var init_tool_definitions = __esm(() => {
18349
18377
  callersOf: {
18350
18378
  type: "string",
18351
18379
  ops: ["profile"],
18352
- description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except one called through another class of the project by name when Class.name was asked, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed."
18380
+ description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed."
18353
18381
  },
18354
18382
  limit: {
18355
18383
  type: "integer",
@@ -18441,7 +18469,7 @@ var init_tool_definitions = __esm(() => {
18441
18469
  },
18442
18470
  {
18443
18471
  name: "editor_status",
18444
- description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
18472
+ description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
18445
18473
  parameters: {},
18446
18474
  requires: []
18447
18475
  },
@@ -48557,7 +48585,7 @@ async function runGodotServer() {
48557
48585
  const server = new GodotServer;
48558
48586
  await server.run();
48559
48587
  }
48560
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless it is called through that class by name and one class was asked about. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
48588
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "';
48561
48589
  var init_server3 = __esm(() => {
48562
48590
  init_mcp();
48563
48591
  init_stdio2();
package/build/index.js CHANGED
@@ -26641,17 +26641,24 @@ function parametersOf(text, from) {
26641
26641
  }
26642
26642
  return names;
26643
26643
  }
26644
- function isLocalAt(lines, index, name) {
26644
+ var DOTTED = `${NAME}(?:\\.${NAME})*`;
26645
+ function declaredClass(text, name) {
26646
+ const word = `(?<![\\p{L}\\p{N}_.])${escapedForPattern(name)}`;
26647
+ const found = new RegExp(`${word}\\s*:\\s*(${DOTTED})`, "u").exec(text)?.[1] ?? new RegExp(`${word}\\s*:=\\s*(${DOTTED})\\.new\\s*\\(`, "u").exec(text)?.[1] ?? new RegExp(`${word}\\s*:?=[^\\n]*?\\bas\\s+(${DOTTED})\\s*$`, "u").exec(text)?.[1];
26648
+ return found === undefined ? null : found.slice(found.lastIndexOf(".") + 1);
26649
+ }
26650
+ function localAt(lines, index, name) {
26645
26651
  const word = escapedForPattern(name);
26646
26652
  const declares = new RegExp(`(?<![\\p{L}\\p{N}_.])(?:var|const)\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
26647
26653
  const iterates = new RegExp(`^\\s*for\\s+${word}(?![\\p{L}\\p{N}_])`, "u");
26654
+ const local = (text) => ({ type: declaredClass(text, name) });
26648
26655
  const own = lines[index] ?? "";
26649
26656
  const headerOnOwnLine = FUNC_LINE.exec(own) ?? LAMBDA_HEADER.exec(own);
26650
26657
  if (headerOnOwnLine !== null && parametersOf(own, headerOnOwnLine.index).includes(name)) {
26651
- return true;
26658
+ return local(own);
26652
26659
  }
26653
26660
  if (FUNC_LINE.test(own)) {
26654
- return false;
26661
+ return null;
26655
26662
  }
26656
26663
  let depth = indentOf(own);
26657
26664
  for (let at = index - 1;at >= 0; at -= 1) {
@@ -26667,21 +26674,21 @@ function isLocalAt(lines, index, name) {
26667
26674
  if (indent < depth) {
26668
26675
  depth = indent;
26669
26676
  if (named !== null) {
26670
- return parametersOf(text, named.index).includes(name);
26677
+ return parametersOf(text, named.index).includes(name) ? local(text) : null;
26671
26678
  }
26672
26679
  const lambda = LAMBDA_HEADER.exec(text);
26673
26680
  if (lambda !== null && parametersOf(text, lambda.index).includes(name) || iterates.test(text)) {
26674
- return true;
26681
+ return local(text);
26675
26682
  }
26676
26683
  }
26677
26684
  if (named !== null || depth === 0) {
26678
- return false;
26685
+ return null;
26679
26686
  }
26680
26687
  if (declares.test(text)) {
26681
- return true;
26688
+ return local(text);
26682
26689
  }
26683
26690
  }
26684
- return false;
26691
+ return null;
26685
26692
  }
26686
26693
  function receiverBefore(code, offset) {
26687
26694
  let at = offset - 1;
@@ -26699,7 +26706,14 @@ function receiverBefore(code, offset) {
26699
26706
  while (start > 0 && isIdentifierPart(code[start - 1])) {
26700
26707
  start -= 1;
26701
26708
  }
26702
- return start <= at ? code.slice(start, at + 1) : null;
26709
+ if (start > at) {
26710
+ return null;
26711
+ }
26712
+ let before = start - 1;
26713
+ while (before >= 0 && /\s/.test(code[before] ?? "")) {
26714
+ before -= 1;
26715
+ }
26716
+ return { name: code.slice(start, at + 1), dotted: code[before] === "." };
26703
26717
  }
26704
26718
  var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
26705
26719
  function bodyHolding(body, offset) {
@@ -26713,13 +26727,27 @@ function callSitesOf(source, name, otherClasses = new Set) {
26713
26727
  `);
26714
26728
  const lines = new Lines(source);
26715
26729
  const shape = shapeOf(source);
26730
+ const memberClass = (member, at) => {
26731
+ const declared = bodyHolding(shape.body, at).declarations.find((one) => one.name === member && (one.kind === "var" || one.kind === "const"));
26732
+ return declared === undefined ? null : declaredClass(codeLines[lines.positionOf(declared.offset).line] ?? "", member);
26733
+ };
26716
26734
  return occurrencesOf(source, name, regions).filter((one) => one.kind === "code" && one.declaredAs === null).filter((one) => {
26735
+ const index = lines.positionOf(one.offset).line;
26717
26736
  if (one.afterDot) {
26718
26737
  const receiver = receiverBefore(code, one.offset);
26719
- return receiver === null || !otherClasses.has(receiver);
26738
+ if (receiver === null || otherClasses.size === 0) {
26739
+ return true;
26740
+ }
26741
+ if (otherClasses.has(receiver.name)) {
26742
+ return false;
26743
+ }
26744
+ if (receiver.dotted) {
26745
+ return true;
26746
+ }
26747
+ const typed = localAt(codeLines, index, receiver.name)?.type ?? memberClass(receiver.name, one.offset);
26748
+ return typed === null || !otherClasses.has(typed);
26720
26749
  }
26721
- const index = lines.positionOf(one.offset).line;
26722
- if (isLocalAt(codeLines, index, name)) {
26750
+ if (localAt(codeLines, index, name) !== null) {
26723
26751
  return false;
26724
26752
  }
26725
26753
  return !bodyHolding(shape.body, one.offset).declarations.some((declared) => declared.name === name && MEMBER_KINDS.has(declared.kind));
@@ -34399,7 +34427,7 @@ var TOOL_SPECS = [
34399
34427
  callersOf: {
34400
34428
  type: "string",
34401
34429
  ops: ["profile"],
34402
- description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except one called through another class of the project by name when Class.name was asked, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed."
34430
+ description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed."
34403
34431
  },
34404
34432
  limit: {
34405
34433
  type: "integer",
@@ -34491,7 +34519,7 @@ var TOOL_SPECS = [
34491
34519
  },
34492
34520
  {
34493
34521
  name: "editor_status",
34494
- description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
34522
+ description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
34495
34523
  parameters: {},
34496
34524
  requires: []
34497
34525
  },
@@ -35850,7 +35878,7 @@ function playedCap(held) {
35850
35878
  totalCapped: readBoolean(held, "totalCapped") === true
35851
35879
  };
35852
35880
  }
35853
- var CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless it is called through that class by name and one class was asked about. Each place carries the profile's numbers for the function it is written in.";
35881
+ var CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.";
35854
35882
  function projectSourceOf(projectPath) {
35855
35883
  const read = new Map;
35856
35884
  return (script) => {
package/build/keeper.js CHANGED
@@ -551,6 +551,7 @@ var CLASS_NAME_LINE = new RegExp(`^${ANNOTATIONS}class_name\\s+(${NAME})`, "u");
551
551
  var EXTENDS_LINE = new RegExp(`^${ANNOTATIONS}(?:class_name\\s+${NAME}\\s+)?extends\\s`, "u");
552
552
  var FUNC_LINE = new RegExp(`^[ \\t]*${ANNOTATIONS}(?:static\\s+)?func\\s+(${NAME})\\s*\\(`, "u");
553
553
  var LAMBDA_HEADER = new RegExp(`(?<![\\p{L}\\p{N}_])func(?:\\s+${NAME})?\\s*\\(`, "u");
554
+ var DOTTED = `${NAME}(?:\\.${NAME})*`;
554
555
  var MEMBER_KINDS = new Set(["var", "const", "signal", "enum", "enumValue"]);
555
556
  var KEYWORDS = new Set([
556
557
  "and",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.61",
3
+ "version": "1.1.62",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",