]> git.wincent.com - wincent.git/commitdiff
feat: add nicer stringification
authorGreg Hurrell <greg@hurrell.net>
Wed, 1 Apr 2020 23:54:33 +0000 (01:54 +0200)
committerGreg Hurrell <greg@hurrell.net>
Wed, 1 Apr 2020 23:54:33 +0000 (01:54 +0200)
For use in debug output, but also in tests.

src/Unicode.ts [new file with mode: 0644]
src/__tests__/stringify-test.ts [new file with mode: 0644]
src/getOptions.ts
src/main.ts
src/stringify.ts [new file with mode: 0644]
src/test/harness.ts

diff --git a/src/Unicode.ts b/src/Unicode.ts
new file mode 100644 (file)
index 0000000..b972c50
--- /dev/null
@@ -0,0 +1,13 @@
+/**
+ * LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
+ *
+ * @see https://www.fileformat.info/info/unicode/char/00ab/index.htm
+ */
+export const LAQUO = '\u00ab';
+
+/**
+ * RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
+ *
+ * @see https://www.fileformat.info/info/unicode/char/00bb/index.htm
+ */
+export const RAQUO = '\u00bb';
diff --git a/src/__tests__/stringify-test.ts b/src/__tests__/stringify-test.ts
new file mode 100644 (file)
index 0000000..8a26c52
--- /dev/null
@@ -0,0 +1,152 @@
+import {expect, test} from '../test/harness';
+import dedent from '../dedent';
+import stringify from '../stringify';
+
+test('stringify() null', () => {
+  expect(stringify(null)).toBe('null');
+});
+
+test('stringify() undefined', () => {
+  expect(stringify(undefined)).toBe('undefined');
+});
+
+test('stringify() true', () => {
+  expect(stringify(true)).toBe('true');
+});
+
+test('stringify() false', () => {
+  expect(stringify(false)).toBe('false');
+});
+
+test('stringify() a number', () => {
+  expect(stringify(9000)).toBe('9000');
+});
+
+test('stringify() a string', () => {
+  expect(stringify('thing')).toBe('"thing"');
+});
+
+test('stringify() a Symbol', () => {
+  expect(stringify(Symbol.for('sample'))).toBe('Symbol(sample)');
+});
+
+test('stringify() a RegExp', () => {
+  expect(stringify(/stuff \w+/i)).toBe('/stuff \\w+/i');
+});
+
+test('stringify() an array', () => {
+  expect(stringify([1, true, 'thing'])).toBe(
+    dedent`
+      [
+        1,
+        true,
+        "thing",
+      ]
+    `.trimEnd()
+  );
+});
+
+test('stringify() nested arrays', () => {
+  expect(stringify([1, true, 'thing', ['nested', null]])).toBe(
+    dedent`
+      [
+        1,
+        true,
+        "thing",
+        [
+          "nested",
+          null,
+        ],
+      ]
+    `.trimEnd()
+  );
+});
+
+test('stringify() an array with circular references', () => {
+  const array: Array<any> = [1, true, 'thing'];
+
+  array.push(array);
+
+  expect(stringify(array)).toBe(
+    dedent`
+      [
+        1,
+        true,
+        "thing",
+        «circular»,
+      ]
+    `.trimEnd()
+  );
+});
+
+test('stringify() an object', () => {
+  expect(stringify({a: 1, b: true})).toBe(
+    dedent`
+      {
+        "a": 1,
+        "b": true,
+      }
+    `.trimEnd()
+  );
+});
+
+test('stringify() a nested object', () => {
+  expect(stringify({a: 1, b: true, c: {d: null}})).toBe(
+    dedent`
+      {
+        "a": 1,
+        "b": true,
+        "c": {
+          "d": null,
+        },
+      }
+    `.trimEnd()
+  );
+});
+
+test('stringify() an object with circular references', () => {
+  const object: {[key: string]: any} = {a: 1, b: true};
+
+  object.c = object;
+
+  expect(stringify(object)).toBe(
+    dedent`
+      {
+        "a": 1,
+        "b": true,
+        "c": «circular»,
+      }
+    `.trimEnd()
+  );
+});
+
+test('stringify() a Date', () => {
+  expect(stringify(new Date())).toBe('[object Date]');
+});
+
+test('stringify() a one-line Function', () => {
+  expect(stringify(() => 1)).toBe('() => 1');
+});
+
+// @ts-ignore: suppress TS7006: Parameter 'a' implicitly has an 'any' type.
+function fn(a, b) {
+  if (a > 0) {
+    return a + b;
+  }
+}
+
+test('stringify() a multi-line Function', () => {
+  // Obviously this test is pretty fragile; depends on TS continuing to
+  // use a 4-space indent in its build output.
+  expect(stringify({fn})).toBe(
+    dedent`
+      {
+        "fn": function fn(a, b) {
+            if (a > 0) {
+                return a + b;
+            }
+        },
+      }
+    `.trimEnd()
+  );
+});
index 0312cbec8f1b152a9d0fa90516f36fb690a0854a..66bea41410fa72f7b411288d0e617cc4f6eb71d0 100644 (file)
@@ -1,4 +1,4 @@
-import {LOG_LEVEL, log, setLogLevel} from './console';
+import {LOG_LEVEL, setLogLevel} from './console';
 import escapeRegExpPattern from './escapeRegExpPattern';
 
 import type {LogLevel} from './console';
index 5fa7622bb12b6b2f2c6bf7e586d7d457988eb02d..13f01bba63503dee1a972a8896546cc01bfa7c14 100644 (file)
@@ -12,6 +12,7 @@ import readAspect from './readAspect';
 import readProject from './readProject';
 import regExpFromString from './regExpFromString';
 import simplify from './simplify';
+import stringify from './stringify';
 import test from './test';
 
 async function main() {
@@ -19,14 +20,13 @@ async function main() {
     throw new ErrorWithMetadata('Cannot run as root');
   }
 
-  const options = getOptions(process.argv);
+  // Skip first two args (node executable and main.js script).
+  const options = getOptions(process.argv.slice(2));
 
   setLogLevel(options.logLevel);
 
-  // argv[0] = node executable
-  // argv[1] = JS script
-  // argv[2] = script arg 0 etc
-  log.debug(JSON.stringify(process.argv, null, 2));
+  log.debug('process.argv:\n\n' + stringify(process.argv) + '\n');
+  log.debug('getOptions():\n\n' + stringify(options) + '\n');
 
   if (process.cwd() === root) {
     log.info(`Working from root: ${simplify(root)}`);
diff --git a/src/stringify.ts b/src/stringify.ts
new file mode 100644 (file)
index 0000000..f90edda
--- /dev/null
@@ -0,0 +1,76 @@
+import {LAQUO, RAQUO} from './Unicode';
+
+const CIRCULAR = `${LAQUO}circular${RAQUO}`;
+
+/**
+ * Basically `JSON.stringify()` but does a better job of printing some value
+ * types (eg. a `RegExp` is printed as "/pattern/" instead of "{}" etc).
+ */
+export default function stringify(value: unknown) {
+  let indent = '';
+
+  const seen = new Set<unknown>();
+
+  function traverse(value: unknown) {
+    if (
+      value == null ||
+      typeof value === 'boolean' ||
+      typeof value === 'number' ||
+      typeof value === 'symbol' ||
+      value instanceof RegExp
+    ) {
+      return String(value);
+    } else if (typeof value === 'string') {
+      return JSON.stringify(value);
+    } else if (Array.isArray(value)) {
+      if (seen.has(value)) {
+        return CIRCULAR;
+      } else {
+        seen.add(value);
+        indent += '  ';
+        let array = '[\n';
+        array += value
+          .map((v) => {
+            return `${indent}${traverse(v)},`;
+          })
+          .join('\n');
+        indent = indent.slice(0, -2);
+        array += `\n${indent}]`;
+        return array;
+      }
+    } else if (typeof value === 'object') {
+      const toString = Object.prototype.toString.call(value);
+      if (toString === '[object Object]') {
+        if (seen.has(value)) {
+          return CIRCULAR;
+        } else {
+          seen.add(value);
+          indent += '  ';
+          let object = '{\n';
+          object += Object.entries(value!)
+            .map(([key, value]) => {
+              return `${indent}${JSON.stringify(key)}: ${traverse(value)},`;
+            })
+            .join('\n');
+          indent = indent.slice(0, -2);
+          object += `\n${indent}}`;
+          return object;
+        }
+      } else {
+        return toString;
+      }
+    } else if (typeof value === 'function') {
+      return value
+        .toString()
+        .split('\n')
+        .map((line, i) => {
+          return i ? `${indent}${line}` : line;
+        })
+        .join('\n');
+    } else {
+      return `${LAQUO}unknown${RAQUO}`;
+    }
+  }
+
+  return traverse(value);
+}
index b61ab0ae9b62b2c722cf888957c7773b3242ada8..5166aab1ea3511007508a3c87202bb46c8f97194 100644 (file)
@@ -1,25 +1,14 @@
 import * as assert from 'assert';
 
 import ErrorWithMetadata from '../ErrorWithMetadata';
-
+import {RAQUO} from '../Unicode';
 import {COLORS, LOG_LEVEL, getLogLevel, log, print} from '../console';
+import stringify from '../stringify';
 
 const {green, red, yellow} = COLORS;
 
 const TESTS: Array<[string, () => void | Promise<void>]> = [];
 
-function stringify(value: unknown) {
-  try {
-    if (value instanceof RegExp) {
-      return value.toString();
-    } else {
-      return JSON.stringify(value);
-    }
-  } catch {
-    return Object.prototype.toString.call(value);
-  }
-}
-
 let context: Array<string> = [];
 
 export function describe(description: string, callback: () => void) {
@@ -100,8 +89,6 @@ export function expect(value: unknown) {
   };
 }
 
-const RAQUO = '\u00bb';
-
 export function test(description: string, callback: () => void) {
   TESTS.push([[...context, description].join(` ${RAQUO} `), callback]);
 }