From adf39edec7ee025beb151bae9db36f62672b44bc Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 5 Jul 2023 17:51:28 +0200 Subject: [PATCH] Add support for inferring type of parents --- index.test-d.ts | 310 +++++++++++++++++++++++------------------------- lib/index.js | 106 ++++++++++++----- 2 files changed, 230 insertions(+), 186 deletions(-) diff --git a/index.test-d.ts b/index.test-d.ts index 4f7cc5b..e036ff6 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,8 +1,50 @@ import {expectAssignable, expectNotType, expectType} from 'tsd' -import type {Literal, Node, Parent} from 'unist' -import {is} from 'unist-util-is' +import type { + Blockquote, + Content, + Definition, + Delete, + Emphasis, + Footnote, + FootnoteDefinition, + Heading, + Link, + LinkReference, + List, + ListItem, + Paragraph, + PhrasingContent, + Root, + Strong, + Table, + TableCell, + TableRow +} from 'mdast' +import type {Node, Parent} from 'unist' import {CONTINUE, EXIT, SKIP, visitParents} from './index.js' +// To do: use `mdast` when released. +type Nodes = Root | Content + +// To do: use `mdast` when released. +type Parents = + | Blockquote + | Delete + | Emphasis + | Footnote + | FootnoteDefinition + | Heading + | Link + | LinkReference + | List + | ListItem + | Paragraph + | Root + | Strong + | Table + | TableCell + | TableRow + /* Setup */ const implicitTree = { type: 'root', @@ -14,179 +56,136 @@ const sampleTree: Root = { children: [{type: 'heading', depth: 1, children: []}] } -const complexTree: Root = { - type: 'root', - children: [ - { - type: 'blockquote', - children: [{type: 'paragraph', children: [{type: 'text', value: 'a'}]}] - }, - { - type: 'paragraph', - children: [ - { - type: 'emphasis', - children: [{type: 'emphasis', children: [{type: 'text', value: 'b'}]}] - }, - {type: 'text', value: 'c'} - ] - } - ] -} - -interface Element extends Parent { - type: 'element' - tagName: string - properties: Record - content: Node - children: Array -} - -type Content = Flow | Phrasing - -interface Root extends Parent { - type: 'root' - children: Array -} - -type Flow = Blockquote | Heading | Paragraph - -interface Blockquote extends Parent { - type: 'blockquote' - children: Array -} - -interface Heading extends Parent { - type: 'heading' - depth: number - children: Array -} - -interface Paragraph extends Parent { - type: 'paragraph' - children: Array -} - -type Phrasing = Text | Emphasis - -interface Emphasis extends Parent { - type: 'emphasis' - children: Array -} - -interface Text extends Literal { - type: 'text' - value: string -} - -const isNode = (node: unknown): node is Node => +const isNode = (node: unknown): node is Nodes => typeof node === 'object' && node !== null && 'type' in node const headingTest = (node: unknown): node is Heading => isNode(node) && node.type === 'heading' -const elementTest = (node: unknown): node is Element => - isNode(node) && node.type === 'element' +const paragraphTest = (node: unknown): node is Paragraph => + isNode(node) && node.type === 'paragraph' -/* Missing params. */ -// @ts-expect-error check. +// ## Missing parameters +// @ts-expect-error: check that `node` is passed. visitParents() -// @ts-expect-error check. +// @ts-expect-error: check that `visitor` is passed. visitParents(sampleTree) -/* Visit without test. */ +// ## No test visitParents(sampleTree, function (node, parents) { - expectType(node) - expectType>(parents) + expectType(node) + expectType>(parents) }) -visitParents(implicitTree, function (node) { + +visitParents(implicitTree, function (node, parents) { + // Objects are too loose. expectAssignable(node) - expectNotType(node) // Objects are too loose. + expectNotType(node) + expectAssignable>(parents) }) -/* Visit with type test. */ +// ## String test + +// Knows it’s a heading and its parents. visitParents(sampleTree, 'heading', function (node, parents) { expectType(node) - // Note that most of these can’t be a parent of `Heading`, but still. - expectType>(parents) + expectType< + Array
+ >(parents) }) -visitParents(sampleTree, 'element', function (node) { - // Not in tree. + +// Not in tree. +visitParents(sampleTree, 'element', function (node, parents) { expectType(node) + expectType>(parents) }) -// @ts-expect-error check. -visitParents(sampleTree, 'heading', function (_: Element) {}) -visitParents(implicitTree, 'heading', function (node) { - expectType(node) // Objects are too loose. - expectAssignable(node) - expectNotType(node) // Objects are too loose. + +// Implicit nodes are too loose. +visitParents(implicitTree, 'heading', function (node, parents) { + expectType(node) + expectType>(parents) }) -/* Visit with object test. */ +// ## Props test + +// Knows that headings have depth, but TS doesn’t infer the depth normally. visitParents(sampleTree, {depth: 1}, function (node) { expectType(node) + expectType<1 | 2 | 3 | 4 | 5 | 6>(node.depth) }) -visitParents(sampleTree, {random: 'property'} as const, function (node) { - expectType(node) + +// This goes fine. +visitParents(sampleTree, {type: 'heading'} as const, function (node) { + expectType(node) + expectType<1 | 2 | 3 | 4 | 5 | 6>(node.depth) }) -visitParents( - sampleTree, - {type: 'heading', depth: '1'} as const, - function (node) { - // Not in tree. - expectType(node) - } -) -visitParents(sampleTree, {tagName: 'section'} as const, function (node) { - // Not in tree. + +// For some reason the const goes wrong. +visitParents(sampleTree, {depth: 1} as const, function (node) { + // Note: something going wrong here, to do: investigate. expectType(node) }) -// @ts-expect-error check. -visitParents(sampleTree, {type: 'heading'} as const, function (_: Element) {}) -visitParents(implicitTree, {type: 'heading'} as const, function (node) { - expectType(node) // Objects are too loose. - expectAssignable(node) - expectNotType(node) // Objects are too loose. +// For some reason the const goes wrong. +visitParents(sampleTree, {type: 'heading', depth: 1} as const, function (node) { + // Note: something going wrong here, to do: investigate. + expectType(node) }) -/* Visit with function test. */ -visitParents(sampleTree, headingTest, function (node) { - expectType(node) +// Function test (implicit assertion). +visitParents(sampleTree, isHeadingLoose, function (node) { + expectType(node) }) -// @ts-expect-error check. -visitParents(sampleTree, headingTest, function (_: Element) {}) -visitParents(sampleTree, elementTest, function (node) { - // Not in tree. - expectType(node) +// Function test (explicit assertion). +visitParents(sampleTree, isHeading, function (node) { + expectType(node) + expectType<1 | 2 | 3 | 4 | 5 | 6>(node.depth) }) -visitParents(implicitTree, headingTest, function (node) { - expectType(node) // Objects are too loose. - expectAssignable(node) - expectNotType(node) // Objects are too loose. +// Function test (explicit assertion). +visitParents(sampleTree, isHeading2, function (node) { + expectType(node) }) -/* Visit with array of tests. */ -visitParents(sampleTree, ['heading', {depth: 1}, headingTest], function (node) { +// ## Combined tests +visitParents(sampleTree, ['heading', {depth: 1}, isHeading], function (node) { // Unfortunately TS casts things in arrays too vague. expectType(node) }) -/* Visit returns action. */ +// To do: update to `unist-util-is` should make this work? +// visitParents( +// sampleTree, +// ['heading', {depth: 1}, isHeading] as const, +// function (node) { +// // Unfortunately TS casts things in arrays too vague. +// expectType(node) +// } +// ) + +// ## Return type: incorrect. +// @ts-expect-error: not an action. visitParents(sampleTree, function () { - return CONTINUE + return 'random' }) +// @ts-expect-error: not a tuple: missing action. visitParents(sampleTree, function () { - return EXIT + return [1] }) +// @ts-expect-error: not a tuple: incorrect action. visitParents(sampleTree, function () { - return SKIP + return ['random', 1] }) -// @ts-expect-error check. +// ## Return type: action. visitParents(sampleTree, function () { - return 'random' + return CONTINUE +}) +visitParents(sampleTree, function () { + return EXIT +}) +visitParents(sampleTree, function () { + return SKIP }) -/* Visit returns index. */ +// ## Return type: index. visitParents(sampleTree, function () { return 0 }) @@ -194,7 +193,7 @@ visitParents(sampleTree, function () { return 1 }) -/* Visit returns tuple. */ +// ## Return type: tuple. visitParents(sampleTree, function () { return [CONTINUE, 1] }) @@ -208,40 +207,33 @@ visitParents(sampleTree, function () { return [SKIP] }) -// @ts-expect-error check. -visitParents(sampleTree, function () { - return [1] -}) - -// @ts-expect-error check. -visitParents(sampleTree, function () { - return ['random', 1] +// ## Infer on tree +visitParents(sampleTree, 'tableCell', function (node) { + visitParents(node, function (node, parents) { + expectType(node) + expectType< + Array< + Delete | Emphasis | Footnote | Link | LinkReference | Strong | TableCell + > + >(parents) + }) }) -/* Should infer children from the given tree. */ -visitParents(complexTree, function (node) { - expectType(node) +visitParents(sampleTree, 'definition', function (node) { + visitParents(node, function (node, parents) { + expectType(node) + expectType>(parents) + }) }) -const blockquote = complexTree.children[0] -if (is
(blockquote, 'blockquote')) { - visitParents(blockquote, function (node) { - expectType(node) - }) +function isHeading(node: Node): node is Heading { + return node ? node.type === 'heading' : false } -const paragraph = complexTree.children[1] -if (is(paragraph, 'paragraph')) { - visitParents(paragraph, function (node) { - expectType(node) - }) - - const child = paragraph.children[1] +function isHeading2(node: Node): node is Heading & {depth: 2} { + return isHeading(node) && node.depth === 2 +} - if (is(child, 'emphasis')) { - visitParents(child, 'blockquote', function (node) { - // `blockquote` does not exist in phrasing. - expectType(node) - }) - } +function isHeadingLoose(node: Node) { + return node ? node.type === 'heading' : false } diff --git a/lib/index.js b/lib/index.js index 6559114..059a6d0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,6 @@ /** - * @typedef {import('unist').Node} Node - * @typedef {import('unist').Parent} Parent + * @typedef {import('unist').Node} UnistNode + * @typedef {import('unist').Parent} UnistParent * @typedef {import('unist-util-is').Test} Test */ @@ -19,17 +19,17 @@ /** * @typedef {( - * Value extends Node - * ? Check extends null | undefined // No test. - * ? Value - * : Check extends Function // Function test. - * ? Extract> - * : Value['type'] extends Check // String (type) test. - * ? Value - * : Value extends Check // Partial test. - * ? Value + * Check extends null | undefined // No test. + * ? Value + * : Value extends {type: Check} // String (type) test. + * ? Value + * : Value extends Check // Partial test. + * ? Value + * : Check extends Function // Function test. + * ? Predicate extends Value + * ? Predicate * : never - * : never + * : never // Some other test? * )} MatchesOne * Check whether a node matches a primitive check in the type system. * @template Value @@ -65,7 +65,59 @@ /** * @typedef {( - * Tree extends Parent + * Node extends UnistParent + * ? Node extends {children: Array} + * ? Child extends Children ? Node : never + * : never + * : never + * )} InternalParent + * Collect nodes that can be parents of `Child`. + * @template {UnistNode} Node + * All node types in a tree. + * @template {UnistNode} Child + * Node to search for. + */ + +/** + * @typedef {InternalParent, Child>} Parent + * Collect nodes in `Tree` that can be parents of `Child`. + * @template {UnistNode} Tree + * All node types in a tree. + * @template {UnistNode} Child + * Node to search for. + */ + +/** + * @typedef {( + * Depth extends Max + * ? never + * : + * | InternalParent + * | InternalAncestor, Max, Increment> + * )} InternalAncestor + * Collect nodes in `Tree` that can be ancestors of `Child`. + * @template {UnistNode} Node + * All node types in a tree. + * @template {UnistNode} Child + * Node to search for. + * @template {Uint} [Max=10] + * Max; searches up to this depth. + * @template {Uint} [Depth=0] + * Current depth. + */ + +/** + * @typedef {InternalAncestor, Child>} Ancestor + * Collect nodes in `Tree` that can be ancestors of `Child`. + * @template {UnistNode} Tree + * All node types in a tree. + * @template {UnistNode} Child + * Node to search for. + */ + +/** + * @typedef {( + * Tree extends UnistParent * ? Depth extends Max * ? Tree * : Tree | InclusiveDescendant> @@ -81,7 +133,7 @@ * > passed, but it doesn’t improve performance. * > It gets higher with `List > ListItem > Table > TableRow > TableCell`. * > Using up to `10` doesn’t hurt or help either. - * @template {Node} Tree + * @template {UnistNode} Tree * Tree type. * @template {Uint} [Max=10] * Max; searches up to this depth. @@ -129,7 +181,7 @@ * traversed. * @param {Visited} node * Found node. - * @param {Array} ancestors + * @param {Array} ancestors * Ancestors of `node`. * @returns {VisitorResult} * What to do next. @@ -140,18 +192,18 @@ * Passing a tuple back only makes sense if the `Action` is `SKIP`. * When the `Action` is `EXIT`, that action can be returned. * When the `Action` is `CONTINUE`, `Index` can be returned. - * @template {Node} [Visited=Node] + * @template {UnistNode} [Visited=UnistNode] * Visited node type. - * @template {Parent} [Ancestor=Parent] + * @template {UnistParent} [VisitedParents=UnistParent] * Ancestor type. */ /** - * @typedef {Visitor, Check>, Extract, Parent>>} BuildVisitor + * @typedef {Visitor, Check>, Ancestor, Check>>>} BuildVisitor * Build a typed `Visitor` function from a tree and a test. * * It will infer which values are passed as `node` and which as `parents`. - * @template {Node} [Tree=Node] + * @template {UnistNode} [Tree=UnistNode] * Tree type. * @template {Test} [Check=Test] * Test type. @@ -209,18 +261,18 @@ export const SKIP = 'skip' * @param {boolean | null | undefined} [reverse] * @returns {undefined} * - * @param {Node} tree + * @param {UnistNode} tree * Tree to traverse. - * @param {Visitor | Test} test + * @param {Visitor | Test} test * `unist-util-is`-compatible test - * @param {Visitor | boolean | null | undefined} [visitor] + * @param {Visitor | boolean | null | undefined} [visitor] * Handle each node. * @param {boolean | null | undefined} [reverse] * Traverse in reverse preorder (NRL) instead of the default preorder (NLR). * @returns {undefined} * Nothing. * - * @template {Node} Tree + * @template {UnistNode} Tree * Node type. * @template {Test} Check * `unist-util-is`-compatible test. @@ -244,9 +296,9 @@ export function visitParents(tree, test, visitor, reverse) { factory(tree, undefined, [])() /** - * @param {Node} node + * @param {UnistNode} node * @param {number | undefined} index - * @param {Array} parents + * @param {Array} parents */ function factory(node, index, parents) { const value = /** @type {Record} */ ( @@ -278,7 +330,7 @@ export function visitParents(tree, test, visitor, reverse) { let subresult /** @type {number} */ let offset - /** @type {Array} */ + /** @type {Array} */ let grandparents if (!test || is(node, index, parents[parents.length - 1] || undefined)) { @@ -291,7 +343,7 @@ export function visitParents(tree, test, visitor, reverse) { } if ('children' in node && node.children) { - const nodeAsParent = /** @type {Parent} */ (node) + const nodeAsParent = /** @type {UnistParent} */ (node) if (nodeAsParent.children && result[0] !== SKIP) { offset = (reverse ? nodeAsParent.children.length : -1) + step