const { DebuggerValue, JavaType, LiteralValue, VariableValue } = require('./debugger-types'); const { assignVariable } = require('./expression/assign'); const { NumberBaseConverter } = require('./utils/nbc'); const { VariableManager } = require('./variable-manager'); /** * @typedef {import('./debugger').Debugger} Debugger * @typedef {import('./debugger-types').DebuggerFrameInfo} DebuggerFrameInfo */ /** * @param {DebuggerValue[]} variables * @param {boolean} thisFirst * @param {boolean} allCapsLast */ function sortVariables(variables, thisFirst, allCapsLast) { return variables.sort((a,b) => { if (a.name === b.name) return 0; if (thisFirst) { if (a.name === 'this') return -1; if (b.name === 'this') return +1; } if (allCapsLast) { const acaps = !/[a-z]/.test(a.name); const bcaps = !/[a-z]/.test(b.name); if (acaps !== bcaps) { return acaps ? +1 : -1; } } return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }); }); } class DebuggerStackFrame extends VariableManager { /** * @param {Debugger} dbgr * @param {DebuggerFrameInfo} frame * @param {VSCVariableReference} frame_variable_reference */ constructor(dbgr, frame, frame_variable_reference) { super(frame_variable_reference ); this.variableReference = frame_variable_reference; this.dbgr = dbgr; this.frame = frame; /** @type {DebuggerValue[]} */ this.locals = null; } /** * Return the list of local values for this stack frame * @returns {Promise} */ async getLocals() { if (this.locals) { return this.locals; } const fetch_locals = async () => { const values = await this.dbgr.getLocals(this.frame); // display the variables in (case-insensitive) alphabetical order, with 'this' first return this.locals = sortVariables(values, true, false); } // @ts-ignore return this.locals = fetch_locals(); } async getLocalVariables() { const values = await this.getLocals(); return values.map(value => this.makeVariableValue(value)); } /** * @param {VSCVariableReference} variablesReference * @param {string} name * @param {DebuggerValue} value */ async setVariableValue(variablesReference, name, value) { /** @type {DebuggerValue[]} */ let variables; if (variablesReference === this.variableReference) { variables = this.locals; } else { const varinfo = this.variableValues.get(variablesReference); if (!varinfo || !varinfo.cached) { throw new Error(`Variable '${name}' not found`); } variables = varinfo.cached; } const var_idx = variables.findIndex(v => v.name === name); try { const updated_value = await assignVariable(this.dbgr, variables[var_idx], name, value); variables[var_idx] = updated_value; return this.makeVariableValue(updated_value); } catch(e) { throw new Error(`Variable update failed. ${e.message}`); } } /** * @param {VSCVariableReference} variablesReference * @returns {Promise} */ async getExpandableValues(variablesReference) { const varinfo = this.variableValues.get(variablesReference); if (!varinfo) { return []; } if (varinfo.cached) { // return the cached version return varinfo.cached.map(v => this.makeVariableValue(v)); } if (varinfo.primitive) { // convert the primitive value into alternate formats return this.getPrimitive(varinfo); } /** @type {DebuggerValue[]} */ let values = []; if (varinfo.objvar) { // object fields request values = sortVariables(await this.getObjectFields(varinfo), false, true); } else if (varinfo.arrvar) { // array elements request const arr = await this.getArrayElements(varinfo); if (arr.isSubrange) { // @ts-ignore return arr.values; } // @ts-ignore values = arr.values; } else if (varinfo.bigstring) { values = [await this.getBigString(varinfo)]; } return (varinfo.cached = values).map(v => this.makeVariableValue(v, varinfo.display_format)); } async getObjectFields(varinfo) { const supertype = await this.dbgr.getSuperType(varinfo.objvar.type.signature); const fields = await this.dbgr.getFieldValues(varinfo.objvar); // add an extra msg field for exceptions if (varinfo.exception) { const call = await this.dbgr.invokeToString(varinfo.objvar.value, varinfo.threadid, varinfo.objvar.type.signature); call.name = ":message"; fields.unshift(call); } // add a ":super" member, unless the super is Object if (supertype && supertype.signature !== JavaType.Object.signature) { fields.unshift(new DebuggerValue('super', supertype, varinfo.objvar.value, true, false, ':super', null)); } return fields; } async getArrayElements(varinfo) { const range = varinfo.range, count = range[1] - range[0]; // should always have a +ve count, but just in case... if (count <= 0) { return null; } // counts over 110 are shown as subranges if (count > 110) { return { isSubrange: true, values: this.getArraySubrange(varinfo.arrvar, count, range), }; } // get the elements for the specified range const elements = await this.dbgr.getArrayElementValues(varinfo.arrvar, range[0], count); return { isSubrange: false, values: elements, } } /** * * @param {*} arrvar * @param {number} count * @param {[number,number]} range */ getArraySubrange(arrvar, count, range) { // create subranges in the sub-power of 10 const subrangelen = Math.max(Math.pow(10, (Math.log10(count)|0)-1),100); /** @type {VariableValue[]} */ const variables = []; for (let i = range[0]; i < range[1]; i+= subrangelen) { const varinfo = { varref: 0, arrvar, range: [i, Math.min(i+subrangelen, range[1])], }; const varref = this._addVariable(varinfo); const variable = new VariableValue(`[${varinfo.range[0]}..${varinfo.range[1]-1}]`, '', null, varref, ''); variables.push(variable); } return variables; } async getBigString(varinfo) { const string = await this.dbgr.getStringText(varinfo.bigstring.value); const res = new LiteralValue(JavaType.String, string); res.name = ''; res.string = string; return res; } getPrimitive(varinfo) { /** @type {VariableValue[]} */ const variables = []; const bits = { J:64, I:32, S:16, B:8, }[varinfo.signature]; /** * * @param {number|hex64} n * @param {number} base * @param {number} len */ function convert(n, base, len) { let converted; if (typeof n === 'string') { converted = { 2: () => n.replace(/./g, c => parseInt(c,16).toString(2)), 10: () => NumberBaseConverter.hexToDec(n, false), 16: () => n, }[base](); } else { converted = n.toString(base); } return converted.padStart(len, '0'); } /** * @param {number|hex64} u * @param {8|16|32|64} bits */ function getIntFormats(u, bits) { const bases = [2, 10, 16]; const min_lengths = [bits, 1, bits/4]; const base_names = ['', '', '']; return base_names.map((name, i) => new VariableValue(name, convert(u, bases[i], min_lengths[i]))); } switch(varinfo.signature) { case 'Ljava/lang/String;': variables.push(new VariableValue('', varinfo.value.toString())); break; case 'C': variables.push(new VariableValue('', varinfo.value.charCodeAt(0).toString())); break; case 'J': // because JS cannot handle 64bit ints, we need a bit of extra work const v64hex = varinfo.value.replace(/[^0-9a-fA-F]/g,''); variables.push(...getIntFormats(v64hex, 64)); break; default:// integer/short/byte value const u = varinfo.value >>> 0; variables.push(...getIntFormats(u, bits)); break; } return variables; } } module.exports = { DebuggerStackFrame, }