Skip to content

Add JSDoc based types #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.DS_Store
*.d.ts
*.log
coverage/
node_modules/
Expand Down
229 changes: 176 additions & 53 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,142 @@
/**
* @typedef {import('assert').AssertionError} AssertionError
* @typedef {import('unist').Node} Node
* @typedef {import('unist').Parent} Parent
* @typedef {import('unist').Literal} Literal
* @typedef {import('unist').Position} Position
* @typedef {import('unist').Point} Point
* @typedef {Node & {children: never, value: never}} Void
*/

import nodeAssert from 'assert'
import {inspect} from './inspect.js'

export var assert = wrap(assertNode)
assert.parent = wrap(parent)
assert.text = wrap(text)
assert.void = wrap(empty)
var own = {}.hasOwnProperty

/**
* Assert that `node` is a valid unist node.
* If `node` is a parent, all children will be asserted too.
*
* @param {unknown} [node]
* @param {Parent} [parent]
* @returns {asserts node is Node}
*/
export function assert(node, parent) {
return wrap(assertNode)(node, parent)
}

/**
* Assert that `node` is a valid unist parent.
* All children will be asserted too.
*
* @param {unknown} [node]
* @param {Parent} [parent]
* @returns {asserts node is Parent}
*/
export function parent(node, parent) {
return wrap(assertParent)(node, parent)
}

/**
* Assert that `node` is a valid unist literal.
*
* @param {unknown} [node]
* @param {Parent} [parent]
* @returns {asserts node is Literal}
*/
export function literal(node, parent) {
return wrap(assertLiteral)(node, parent)
}

/**
* Assert that `node` is a valid unist node, but neither parent nor literal.
*
* @param {unknown} [node]
* @param {Parent} [parent]
* @returns {asserts node is Void}
*/
export function _void(node, parent) {
return wrap(assertVoid)(node, parent)
}

// Identifier to check if a value is seen.
var ID = '__unist__'

// List of specced properties.
var defined = new Set(['type', 'value', 'children', 'position'])

// Wrapper around `Node` which adds the current node (and parent, if available),
// to the message.
/**
* Wrapper that adds the current node (and parent, if available) to error
* messages.
*
* @param {(node?: unknown, parent?: Parent|null) => asserts node is Node} fn
* @returns {(node?: unknown, parent?: Parent|null) => asserts node is Node}
*/
export function wrap(fn) {
return wrapped

/**
* @param {unknown} node
* @param {Parent} [parent]
* @throws {AssertionError}
* @returns {asserts node is T}
*/
function wrapped(node, parent) {
try {
fn(node, parent)
} catch (error) {
if (!error[ID]) {
if (!own.call(error, ID)) {
error[ID] = true

error.message += ': `' + view(node) + '`'

if (parent) {
error.message += ' in `' + view(parent) + '`'
}
if (parent) error.message += ' in `' + view(parent) + '`'
}

throw error
}
}
}

// Assert.
/**
* Assert.
*
* @param {unknown} node
* @returns {asserts node is Node}
*/
function assertNode(node) {
var type
var children
var value
var index = -1
/** @type {Parent} */
var parent
/** @type {unknown} */
var child
/** @type {string} */
var key
var index
var length

nodeAssert.ok(node === Object(node), 'node should be an object')

type = node.type
children = node.children
value = node.value
nodeAssert.ok(
node && typeof node === 'object' && !Array.isArray(node),
'node should be an object'
)

nodeAssert.ok('type' in node, 'node should have a type')
nodeAssert.strictEqual(typeof type, 'string', '`type` should be a string')
nodeAssert.notStrictEqual(type, '', '`type` should not be empty')
nodeAssert.ok(own.call(node, 'type'), 'node should have a type')
nodeAssert.strictEqual(
// @ts-expect-error Looks like an indexed object.
typeof node.type,
'string',
'`type` should be a string'
)
// @ts-expect-error Looks like an indexed object.
nodeAssert.notStrictEqual(node.type, '', '`type` should not be empty')

if (value !== null && value !== undefined) {
nodeAssert.strictEqual(typeof value, 'string', '`value` should be a string')
// @ts-expect-error Looks like an indexed object.
if (node.value !== null && node.value !== undefined) {
nodeAssert.strictEqual(
// @ts-expect-error Looks like an indexed object.
typeof node.value,
'string',
'`value` should be a string'
)
}

// @ts-expect-error Looks like an indexed object.
position(node.position)

for (key in node) {
Expand All @@ -67,19 +145,30 @@ function assertNode(node) {
}
}

if (children !== null && children !== undefined) {
nodeAssert.ok(Array.isArray(children), '`children` should be an array')
// @ts-expect-error Looks like an indexed object.
if (node.children !== null && node.children !== undefined) {
// @ts-expect-error Looks like parent.
parent = node
nodeAssert.ok(
Array.isArray(parent.children),
'`children` should be an array'
)
index = -1
length = children.length

while (++index < length) {
assert(children[index], node)
while (++index < parent.children.length) {
child = parent.children[index]
assert(child, parent)
}
}
}

// Assert `value` (which lives at `key`) can be stringified and re-parsed to the
// same (deep) value.
/**
* Assert `value` (which lives at `key`) can be stringified and re-parsed to the
* same (deep) value.
*
* @param {string} key
* @param {unknown} value
*/
function vanilla(key, value) {
try {
nodeAssert.deepStrictEqual(value, JSON.parse(JSON.stringify(value)))
Expand All @@ -88,8 +177,13 @@ function vanilla(key, value) {
}
}

// Stringify a value to inspect it.
// Tries `JSON.stringify()`, and if that fails uses `String()` instead.
/**
* Stringify a value to inspect it.
* Tries `JSON.stringify()`, and if that fails uses `String()` instead.
*
* @param {unknown} value
* @returns {string}
*/
function view(value) {
try {
return inspect(value)
Expand All @@ -99,8 +193,13 @@ function view(value) {
}
}

// Assert `node` is a parent node.
function parent(node) {
/**
* Assert `node` is a parent node.
*
* @param {Node} node
* @returns {asserts node is Parent}
*/
function assertParent(node) {
assertNode(node)

nodeAssert.strictEqual(
Expand All @@ -111,20 +210,30 @@ function parent(node) {
nodeAssert.ok('children' in node, 'parent should have `children`')
}

// Assert `node` is a text node.
function text(node) {
/**
* Assert `node` is a literal node.
*
* @param {Node} node
* @returns {asserts node is Literal}
*/
function assertLiteral(node) {
assertNode(node)

nodeAssert.strictEqual(
'children' in node,
false,
'text should not have `children`'
'literal should not have `children`'
)
nodeAssert.ok('value' in node, 'text should have `value`')
nodeAssert.ok('value' in node, 'literal should have `value`')
}

// Assert `node` is a unist node, but neither parent nor text.
function empty(node) {
/**
* Assert `node` is a unist node, but neither parent nor literal.
*
* @param {Node} node
* @returns {asserts node is Void}
*/
function assertVoid(node) {
assertNode(node)

nodeAssert.strictEqual('value' in node, false, 'void should not have `value`')
Expand All @@ -135,7 +244,12 @@ function empty(node) {
)
}

// Assert `position` is a unist position.
/**
* Assert `position` is a unist position.
*
* @param {Position} position
* @returns {asserts position is Position}
*/
function position(position) {
if (position !== null && position !== undefined) {
nodeAssert.ok(
Expand All @@ -148,27 +262,36 @@ function position(position) {
}
}

// Assert `point` is a unist point.
function point(point, name) {
/**
* Assert `point` is a unist point.
*
* @param {Point} point
* @param {string} label
* @returns {asserts point is Point}
*/
function point(point, label) {
if (point !== null && point !== undefined) {
nodeAssert.ok(point === Object(point), '`' + name + '` should be an object')
nodeAssert.ok(
point === Object(point),
'`' + label + '` should be an object'
)

if (point.line !== null && point.line !== undefined) {
nodeAssert.ok(
'line' in point,
'`' + name + '` should have numeric `line`'
'`' + label + '` should have numeric `line`'
)
nodeAssert.ok(point.line >= 1, '`' + name + '.line` should be gte `1`')
nodeAssert.ok(point.line >= 1, '`' + label + '.line` should be gte `1`')
}

if (point.column !== null && point.column !== undefined) {
nodeAssert.ok(
'column' in point,
'`' + name + '` should have numeric `column`'
'`' + label + '` should have numeric `column`'
)
nodeAssert.ok(
point.column >= 1,
'`' + name + '.column` should be gte `1`'
'`' + label + '.column` should be gte `1`'
)
}
}
Expand Down
18 changes: 18 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {expectType, expectNotType} from 'tsd'
import {Node, Parent} from 'unist'
import {assert, parent} from './index.js'

var emptyNode = {type: 'a'}
var literalNode = {type: 'b', value: 'c'}
var parentNode = {type: 'd', children: [emptyNode, literalNode]}

expectNotType<Node>(emptyNode)
expectNotType<Node>(literalNode)
expectNotType<Node>(parentNode)

assert(emptyNode)
expectType<Node>(emptyNode)

expectNotType<Parent>(parentNode)
parent(parentNode)
expectType<Parent>(parentNode)
4 changes: 4 additions & 0 deletions inspect.browser.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* @param {unknown} value
* @returns {string}
*/
export function inspect(value) {
return JSON.stringify(value)
}
4 changes: 4 additions & 0 deletions inspect.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import {inspect as utilInspect} from 'util'

/**
* @param {unknown} value
* @returns {string}
*/
export function inspect(value) {
return utilInspect(value, {colors: false})
}
Loading