Skip to content

Commit

Permalink
Better human logging of classes and functions (#439)
Browse files Browse the repository at this point in the history
This PR rounds out the recent round of logging improvements by getting
function and class objects to get encoded as sexps and then rendered in
ways that are more suggestive of their actual identities, as opposed to
just getting rendered like literal strings.
  • Loading branch information
danfuzz authored Nov 22, 2024
2 parents 3f638ef + a52ecb2 commit 83f55b5
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 46 deletions.
22 changes: 19 additions & 3 deletions src/loggy-intf/export/LoggedValueEncoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class LoggedValueEncoder extends BaseValueVisitor {

/** @override */
_impl_visitClass(node) {
return this._prot_nameFromValue(node);
return this.#visitFunctionOrClass(node, true);
}

/** @override */
Expand Down Expand Up @@ -95,7 +95,7 @@ export class LoggedValueEncoder extends BaseValueVisitor {

/** @override */
_impl_visitFunction(node) {
return this._prot_nameFromValue(node);
return this.#visitFunctionOrClass(node, false);
}

/** @override */
Expand All @@ -108,7 +108,7 @@ export class LoggedValueEncoder extends BaseValueVisitor {
const constructor = Reflect.getPrototypeOf(node).constructor;
const ELIDED = LoggedValueEncoder.#SEXP_ELIDED;
return constructor
? new Sexp(this._prot_nameFromValue(constructor), ELIDED)
? new Sexp(this._prot_visitSync(constructor), ELIDED)
: new Sexp('Object', this._prot_labelFromValue(node), ELIDED);
}
}
Expand Down Expand Up @@ -144,6 +144,22 @@ export class LoggedValueEncoder extends BaseValueVisitor {
return new Sexp('Undefined');
}

/**
* Transforms a function or class into the corresponding {@link Sexp} form.
*
* @param {function()} node Function or class to convert.
* @param {boolean} isClass Is it considered a class (that is, only usable as
* a constructor)?
* @returns {Sexp} The converted form.
*/
#visitFunctionOrClass(node, isClass) {
const name = node.name;
const anonymous = !(name && (name !== ''));
const nameArgs = anonymous ? [] : [name];

return new Sexp(isClass ? 'Class' : 'Function', ...nameArgs);
}

//
// Static members
//
Expand Down
107 changes: 86 additions & 21 deletions src/loggy-intf/private/HumanVisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,27 +94,7 @@ export class HumanVisitor extends BaseValueVisitor {
}
return new ComboText(...result);
} else if (node instanceof Sexp) {
const { functorName, args } = node;
switch (functorName) {
case 'BigInt': {
const str = `${args[0]}n`;
return this.#maybeStyle(str, HumanVisitor.#STYLE_NUMBER);
}
case 'Elided': {
return this.#maybeStyle('...', HumanVisitor.#STYLE_ELIDED);
}
case 'Symbol': {
const funcStr = args[1] ? 'Symbol.for' : 'Symbol';
const symArgs = (args[0] === null) ? [] : [args[0]];
return this.#visitCall(funcStr, symArgs);
}
case 'Undefined': {
return this.#maybeStyle('undefined', HumanVisitor.#STYLE_UNDEFINED);
}
default: {
return this.#visitCall(`@${functorName}`, args, HumanVisitor.#STYLE_SEXP);
}
}
return this.#renderSexp(node);
} else {
throw this.#shouldntHappen();
}
Expand Down Expand Up @@ -171,6 +151,26 @@ export class HumanVisitor extends BaseValueVisitor {
: text;
}

/**
* Renders a sexp representing a function or class object.
*
* @param {Sexp} sexp The instance to render.
* @param {boolean} isClass Is this a class?
* @returns {TypeText} The rendered form.
*/
#renderFunctionOrClass(sexp, isClass) {
const name = sexp.args[0];
const style = isClass ? HumanVisitor.#STYLE_CLASS : HumanVisitor.#STYLE_FUNCTION;
const label = isClass ? 'class' : 'function';
const fullName = name ? `${label} ${name}` : label;

return [
this.#maybeStyle(`${fullName}`, style),
' ',
this.#maybeStyle('{...}', HumanVisitor.#STYLE_ELIDED)
].join('');
}

/**
* Renders an object key, quoting and colorizing as appropriate.
*
Expand All @@ -186,6 +186,57 @@ export class HumanVisitor extends BaseValueVisitor {
}
}

/**
* Renders a {@link Sexp}.
*
* @param {Sexp} sexp The instance to render.
* @returns {TypeText} The rendered form.
*/
#renderSexp(sexp) {
const { functor, args } = sexp;
let name = functor;

if (functor instanceof Sexp) {
switch (functor.functor) {
case 'Class':
case 'Function': {
name = functor.args[0];
break;
}
default: {
name = functor.functorName;
break;
}
}
} else if (typeof functor === 'string') {
switch (functor) {
case 'BigInt': {
const str = `${args[0]}n`;
return this.#maybeStyle(str, HumanVisitor.#STYLE_NUMBER);
}
case 'Class': {
return this.#renderFunctionOrClass(sexp, true);
}
case 'Elided': {
return this.#maybeStyle('...', HumanVisitor.#STYLE_ELIDED);
}
case 'Function': {
return this.#renderFunctionOrClass(sexp, false);
}
case 'Symbol': {
const funcStr = args[1] ? 'Symbol.for' : 'Symbol';
const symArgs = (args[0] === null) ? [] : [args[0]];
return this.#visitCall(funcStr, symArgs);
}
case 'Undefined': {
return this.#maybeStyle('undefined', HumanVisitor.#STYLE_UNDEFINED);
}
}
}

return this.#visitCall(`@${name}`, args, HumanVisitor.#STYLE_SEXP);
}

/**
* Constructs a "shouldn't happen" error. This is used in the implementation
* of all the `_impl_visit*()` methods corresponding to types that aren't
Expand Down Expand Up @@ -340,13 +391,27 @@ export class HumanVisitor extends BaseValueVisitor {
*/
static #STYLE_DEF_REF = chalk.magenta.bold;

/**
* Styling function to use for class objects.
*
* @type {Function}
*/
static #STYLE_CLASS = chalk.blue.bold;

/**
* Styling function to use for the "elided" value, rendered as `...`.
*
* @type {Function}
*/
static #STYLE_ELIDED = chalk.dim;

/**
* Styling function to use for functions.
*
* @type {Function}
*/
static #STYLE_FUNCTION = chalk.blue.bold;

/**
* Styling function to use for the value `null`.
*
Expand Down
36 changes: 19 additions & 17 deletions src/loggy-intf/tests/LoggedValueEncoder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ function sexp(type, ...args) {
return new Sexp(type, ...args);
}

function sexpClass(type) {
return new Sexp('Class', type);
}

function sexpFunc(type) {
return new Sexp('Function', type);
}

describe('encode()', () => {
// Cases where the result should be equal to the input.
test.each`
Expand Down Expand Up @@ -66,16 +74,21 @@ describe('encode()', () => {

const someFunc = () => null;

// Most stuff that isn't JSON-encodable should end up in the form of a sexp.
// Stuff that isn't JSON-encodable should end up in the form of a sexp.
test.each`
value | expected
${undefined} | ${sexp('Undefined')}}
${[undefined]} | ${[sexp('Undefined')]}}
${321123n} | ${sexp('BigInt', '321123')}}
${Symbol('xyz')} | ${sexp('Symbol', 'xyz')}}
${Symbol.for('blorp')} | ${sexp('Symbol', 'blorp', true)}}
${new Duration(12.34)} | ${sexp('Duration', 12.34, '12.340 sec')}
${new Map()} | ${sexp('Map', sexp('Elided'))}
${new Duration(12.34)} | ${sexp(sexpClass('Duration'), 12.34, '12.340 sec')}
${new Map()} | ${sexp(sexpFunc('Map'), sexp('Elided'))}
${Map} | ${sexp('Function', 'Map')}
${someFunc} | ${sexp('Function', 'someFunc')}
${() => null} | ${sexp('Function')}
${SomeClass} | ${sexp('Class', 'SomeClass')}
${class {}} | ${sexp('Class')}
${new Proxy({}, {})} | ${sexp('Proxy', '<anonymous>')}
${new Proxy([], {})} | ${sexp('Proxy', '<anonymous>')}
${new Proxy(new SomeClass(), {})} | ${sexp('Proxy', '<anonymous>')}
Expand All @@ -85,17 +98,6 @@ describe('encode()', () => {
expect(got).toStrictEqual(expected);
});

test('encodes a function as its name', () => {
const value = someFunc;
const name = value.name;

const got1 = LoggedValueEncoder.encode(value);
expect(got1).toBe(name);

const got2 = LoggedValueEncoder.encode([value, value, value]);
expect(got2).toStrictEqual([name, name, name]);
});

test('does not def-ref a small-enough array', () => {
const value = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const got = LoggedValueEncoder.encode([value, value]);
Expand Down Expand Up @@ -131,7 +133,7 @@ describe('encode()', () => {

test('def-refs the sexp from an instance', () => {
const value = new Map();
const def = new VisitDef(0, sexp('Map', sexp('Elided')));
const def = new VisitDef(0, sexp(sexpFunc('Map'), sexp('Elided')));
const expected = [def, new VisitRef(def)];
const got = LoggedValueEncoder.encode([value, value]);
expect(got).toStrictEqual(expected);
Expand Down Expand Up @@ -173,10 +175,10 @@ describe('encode()', () => {
const outer = new TestClass(inner, inner);
const got = LoggedValueEncoder.encode(outer);

const exInner = new Sexp('TestClass', 'boop');
const exInner = new Sexp(sexpClass('TestClass'), 'boop');
const def = new VisitDef(0, exInner);
const ref = def.ref;
const exOuter = new Sexp('TestClass', def, ref);
const exOuter = new Sexp(sexpClass('TestClass'), def, ref);
expect(got).toStrictEqual(exOuter);
});
});
15 changes: 10 additions & 5 deletions src/sexp/export/Sexp.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class Sexp {
switch (typeof functor) {
case 'function':
case 'object': {
if ((functor === null) || (functor === '')) {
if (functor === null) {
break;
}

Expand Down Expand Up @@ -158,10 +158,15 @@ export class Sexp {
* @returns {*} The replacement form to encode.
*/
toJSON(key_unused) {
const name = this.functorName;
const finalName = (name === '<anonymous>') ? '@' : `@${name}`;

return { [finalName]: this.#args };
const { functor } = this;

if (typeof functor === 'string') {
const name = this.functorName;
const finalName = (name === '<anonymous>') ? '@' : `@${name}`;
return { [finalName]: this.#args };
} else {
return { '@Sexp': this.toArray() };
}
}

/**
Expand Down

0 comments on commit 83f55b5

Please sign in to comment.