0ad/binaries/data/mods/mod/tools/dap/managers/inspector.js
Dunedan 93ce94655d
Use @stylistic/brace-style for eslint
Up to now `eslint-plugin-brace-rules` was used to enforce a common brace
style for JavaScript code. This plugin was however updated the last time
over 9 years ago and will be incompatible with ESLint v10, as that
[removes `context.getSourceCode()`][1], the plugin relies on.

To keep the eslint config working with ESLint v10, this replaces
`eslint-plugin-brace-rules` with the [`@stylistic/brace-style`][2] rule
from `@stylistic/eslint-plugin`, a package we already use.

While `@stylistic/brace-style` doesn't offer an option to format braces
in exactly the same way as before, the "allman" style seems to be the
one closest to the existing code.

[1]: https://eslint.org/blog/2025/11/eslint-v10.0.0-alpha.0-released/#removed-deprecated-rule-context-members
[2]: https://eslint.style/rules/brace-style
2026-01-12 21:33:52 +01:00

388 lines
10 KiB
JavaScript

import { Plugin } from 'tools/dap/plugin.js';
class InspectorManager extends Plugin
{
constructor(jsDebugger, dapHandler)
{
super('InspectorManager', 'manager');
this.variableReferences = [];
this.logger.debug('Setting up InspectorManager');
jsDebugger.on('onDebuggerDetached', () =>
{
this.logger.debug('Debugger attached');
this.variableReferences = [];
}, this.name);
jsDebugger.on('onContinue', () =>
{
this.logger.debug('Continuing execution');
this.variableReferences = [];
}, this.name);
dapHandler.registerCommand('scopes', (req) =>
{
this.logger.debug('Handling scopes command');
if (!jsDebugger.currentFrame)
{
this.logger.error('No current frame available');
return dapHandler.errorResponse(req, 'No current frame available');
}
if (!jsDebugger.currentFrame.onStack || jsDebugger.currentFrame.terminated)
{
this.logger.error('Current frame is not on stack or is terminated');
return dapHandler.errorResponse(req, 'Current frame is not on stack or is terminated');
}
const scopes = [];
let frameId = req.arguments.frameId;
let frame = jsDebugger.currentFrame;
while (frameId > 1)
{
frameId--;
frame = frame.onStack && !frame.terminated ? frame.older : null;
if (!frame)
{
this.logger.error(`Invalid frameId: ${req.arguments.frameId}`);
return dapHandler.errorResponse(req, 'Invalid frameId');
}
}
return dapHandler.successResponse(req, { 'scopes': this.createScopeAndVariableReferences(frame, req.arguments.frameId) });
});
dapHandler.registerCommand('variables', (req) =>
{
this.logger.debug('Handling variables command');
if (!jsDebugger.currentFrame)
{
this.logger.error('No current frame available');
return dapHandler.errorResponse(req, 'No current frame available');
}
const variables = this.expandVariableReference(req.arguments.variablesReference, jsDebugger.currentFrame);
return dapHandler.successResponse(req, { 'variables': variables });
});
dapHandler.registerCommand('evaluate', (req) =>
{
this.logger.debug('Handling evaluate command');
if (!jsDebugger.currentFrame)
{
this.logger.error('No current frame available');
return dapHandler.errorResponse(req, 'No current frame available');
}
const expression = req.arguments.expression;
if (!expression)
{
this.logger.error('No expression provided for evaluation');
return dapHandler.errorResponse(req, 'No expression provided for evaluation');
}
try
{
const result = jsDebugger.currentFrame.eval(expression);
if (result.return instanceof Debugger.Object)
{
const described = this.describeJSObjectVariable(result.return, `eval.${expression}`, { "frameId": req.arguments.frameId }, expression);
described.result = described.value;
return dapHandler.successResponse(req, described);
}
else if (result.return !== undefined)
return dapHandler.successResponse(req, {
'result': JSON.stringify(result.return) || "null",
});
return dapHandler.successResponse(req, { 'result': JSON.stringify(result) || "null", 'variablesReference': 0 });
}
catch(error)
{
this.logger.error(`Error evaluating expression: ${error.message}`);
return dapHandler.errorResponse(req, `Error evaluating expression: ${error.message}`);
}
});
}
createOrUpdateVariableReference(name, data)
{
let index = this.variableReferences.findIndex((ref) => ref.name === name);
if (index === -1)
{
this.variableReferences.push({ name, data });
index = this.variableReferences.length - 1;
}
else
this.variableReferences[index].data = data;
return index + 1;
}
createScopeAndVariableReferences(frame, frameId)
{
if (!frame || !frame.onStack || frame.terminated)
{
this.logger.error(`Invalid frame: ${frameId}`);
return [];
}
const scopes = [];
let environment = frame.environment;
let depth = 0;
while (environment !== null)
{
this.logger.debug(`Creating scope and variable references for environment: ${environment.type}, depth: ${depth}, frameId: ${frameId}, frameType: ${frame.type}`);
const labelParts = [];
const scope = {};
scope.variablesReference = 0;
if (environment.optimizedOut)
labelParts.push('(Optimized Out)');
switch (environment.type)
{
case 'object':
labelParts.push('Global');
break;
case 'width':
labelParts.push('With Scope');
break;
case 'declarative':
if (environment.scopeKind === 'function')
labelParts.push('Closure');
else if (environment.scopeKind === 'function lexical')
labelParts.push('Function Block');
else if (environment.scopeKind)
labelParts.push(environment.scopeKind.charAt(0).toUpperCase() + environment.scopeKind.slice(1));
else
labelParts.push('Declarative');
break;
default:
labelParts.push(environment.type || "Unknown");
}
if (environment.calleeScript)
labelParts.push(`(Script: ${environment.calleeScript.displayName || environment.calleeScript.name || 'unknown'})`);
scope.name = labelParts.join(' ');
const names = (typeof environment.names === "function") ? environment.names() : [];
if (depth === 0 && frame.this instanceof Debugger.Object)
names.unshift('this');
scope.variablesReference = this.createOrUpdateVariableReference(`${frameId}.scope.${depth}`, {
"values": names,
"frameId": frameId,
});
scope.expensive = false;
scopes.push(scope);
environment = environment.parent;
depth++;
}
return scopes;
}
describeJSObjectCallable(jsObject, varName)
{
if (!jsObject || !(jsObject instanceof Debugger.Object) || !jsObject.callable)
{
this.logger.error('Invalid JS Object for callable description');
return {
'name': varName || '(unknown)',
'value': 'undefined',
'variablesReference': 0
};
}
const variable = {
'name': varName,
'variablesReference': 0,
'presentationHint': {
'kind': 'method',
}
};
const funcName = jsObject.displayName || jsObject.name || 'anonymous';
let funcType = 'Function';
if (jsObject.isArrowFunction)
funcType = 'Arrow Function';
else if (jsObject.isClassConstructor)
funcType = 'Class Constructor';
else if (jsObject.isGeneratorFunction)
funcType = 'Generator Function';
else if (jsObject.isAsyncFunction)
funcType = 'Async Function';
else if (jsObject.isBoundFunction)
funcType = 'Bound Function';
const paramNames = (jsObject.parameterNames || []).join(', ');
variable.type = funcType;
variable.value = `[${funcType}] ${funcName}(${paramNames})`;
return variable;
}
describeJSObjectVariable(jsObject, varRefName, varReference, varName)
{
if (!jsObject || !(jsObject instanceof Debugger.Object))
{
this.logger.error('Invalid JS Object for description');
return {
'name': varName || '(unknown)',
'value': 'undefined',
'variablesReference': 0
};
}
if (jsObject.callable)
return this.describeJSObjectCallable(jsObject, varName);
const variable = {
'name': varName,
'variablesReference': 0,
};
switch (jsObject.class)
{
case 'Array':
{
const length = jsObject.getOwnPropertyDescriptor("length")?.value || 0;
variable.value = `[Array] (${length})`;
break;
}
case 'Date':
{
const str = jsObject.unsafeDereference().toString();
variable.value = `[Date] ${str}`;
break;
}
case 'RegExp':
{
const str = jsObject.unsafeDereference().toString();
variable.value = `[RegExp] ${str}`;
break;
}
case 'Map':
case 'Set':
variable.value = `[${jsObject.class}] (${jsObject.getOwnPropertyNames().length})`;
break;
case 'String':
case 'Number':
case 'Boolean':
{
const val = jsObject.unsafeDereference().valueOf();
variable.value = `[${jsObject.class}] ${val}`;
break;
}
default:
{
let label = jsObject.displayName || jsObject.class;
if (jsObject.isProxy)
label = `[Proxy ${label}]`;
else if (jsObject.isPromise)
label = `[Promise ${label}]`;
else if (!jsObject.unsafeDereference)
label = `[Opaque ${label}]`;
else if (jsObject.proto)
{
const proto = jsObject.proto.displayName || jsObject.proto.class || 'unknown';
label = `[${label} (Prototype: ${proto})]`;
}
else
label = `[${label}]`;
variable.value = label;
}
}
const objectProperties = jsObject.getOwnPropertyNames();
if (objectProperties.length === 0)
return variable;
variable.variablesReference = this.createOrUpdateVariableReference(varRefName, {
'values': objectProperties,
'jsObject': jsObject,
'frameId': varReference.frameId
});
return variable;
}
expandVariableReference(variableReferenceIndex, frame)
{
if (variableReferenceIndex === 0 || this.variableReferences.length < variableReferenceIndex - 1)
{
this.logger.warn(`Invalid variable reference index: ${variableReferenceIndex}`);
return [];
}
const varReferenceRef = this.variableReferences[variableReferenceIndex - 1];
const varReference = varReferenceRef.data;
const variables = [];
this.logger.debug(`Expanding variable reference ${variableReferenceIndex}: ${uneval(varReferenceRef)}`);
let frameId = varReference.frameId;
while (frameId > 1)
{
frame = frame.onStack && !frame.terminated ? frame.older : null;
if (!frame)
{
this.logger.error(`Invalid frameId: ${req.arguments.frameId}`);
return dapHandler.errorResponse(req, 'Invalid frameId');
}
frameId--;
}
for (const varName of varReference.values)
{
this.logger.debug(`Expanding variable: ${varName}`);
let value;
if (varReference.jsObject)
value = varReference.jsObject.getOwnPropertyDescriptor(varName)?.value;
else if (varName === 'this' && frame.this instanceof Debugger.Object)
value = frame.this;
else
{
const valueEnv = frame.environment.find(varName);
if (valueEnv === null)
{
this.logger.warn(`Could not find variable ${varName}`);
continue;
}
if (valueEnv.type === 'object' || valueEnv.type === 'with')
value = valueEnv.object.getOwnPropertyDescriptor(varName)?.value;
else
value = valueEnv.getVariable(varName);
}
if (value instanceof Debugger.Object)
{
variables.push(this.describeJSObjectVariable(value, `${varReferenceRef.name}.${varName}`, varReference, varName));
continue;
}
variables.push({
'name': varName,
'variablesReference': 0,
'value': JSON.stringify(value) || "null"
});
}
return variables;
}
}
export default InspectorManager;