types: add export to package.json, test with strict mode and node16 resolution #34
Conversation
9c138cd to
7cc986c
Compare
| this.should_skip = false; | ||
|
|
||
| /** @type {boolean} */ | ||
| this.should_remove = false; | ||
|
|
||
| /** @type {Node | null} */ | ||
| this.replacement = null; | ||
|
|
||
| /** @type {WalkerContext} */ | ||
| this.context = { | ||
| skip: () => (this.should_skip = true), | ||
| remove: () => (this.should_remove = true), | ||
| replace: (node) => (this.replacement = node) | ||
| }; |
There was a problem hiding this comment.
These shouldn't need to be here, they are defined by the super() calling WalkerBase.
But TypeScript + JSDoc seems to be unable to infer these from the extends WalkerBase on the class. 🤔
/cc @wooorm @remcohaszing have you ever run into this before? Do you know of any work arounds?
There was a problem hiding this comment.
Apparently the culprit are these lines in visit:
const _should_skip = this.should_skip;
const _should_remove = this.should_remove;
const _replacement = this.replacement;
// …
this.should_skip = _should_skip;
this.should_remove = _should_remove;
this.replacement = _replacement;It appears TypeScript is having problems recursively inferring these types from themselves. As a workaround, you can type-cast the values that are later assigned back. Note that this occurs twice!
const _should_skip = /** @type {boolean} */ (this.should_skip);
const _should_remove = /** @type {boolean} */ (this.should_remove);
const _replacement = /** @type {Node | null} */ (this.replacement);This is definitely a bug in TypeScript. Here’s minimal reproduction. See what happens if you comment the line this.name = name.
class Foo {
name = ''
}
class Bar extends Foo {
rename() {
const name = this.name
this.name = name
}
}There was a problem hiding this comment.
Maybe you need to type fields? Such as like this: https://github.com/vfile/vfile/blob/03efac7cc3a67fe33bb7a0f1de29e17be322549e/lib/index.js#L122-L126
There was a problem hiding this comment.
No, it’s currently done that way, but it doesn’t make a difference. It’s a bug with subclasses. Also it only happens in JS. If the exact same code in the minimal reproduction is added in a .ts file, TypeScript can infer the types perfectly fine.
| replace: (node) => (this.replacement = node) | ||
| }; | ||
|
|
||
| /** @type {AsyncHandler | undefined} */ |
There was a problem hiding this comment.
If you assign this.whatever = whatever in the constructor, TypeScript can infer its type, meaning this type annotation is redundant. There are multiple occurrences across multiple files. It’s not wrong, but personally I would omit them.
| this.should_skip = false; | ||
|
|
||
| /** @type {boolean} */ | ||
| this.should_remove = false; | ||
|
|
||
| /** @type {Node | null} */ | ||
| this.replacement = null; | ||
|
|
||
| /** @type {WalkerContext} */ | ||
| this.context = { | ||
| skip: () => (this.should_skip = true), | ||
| remove: () => (this.should_remove = true), | ||
| replace: (node) => (this.replacement = node) | ||
| }; |
There was a problem hiding this comment.
No, it’s currently done that way, but it doesn’t make a difference. It’s a bug with subclasses. Also it only happens in JS. If the exact same code in the minimal reproduction is added in a .ts file, TypeScript can infer the types perfectly fine.
|
@ChristianMurphy What’s blocking this? |
Time, more specifically a lack thereof. I think there are two separate things which need to be resolved in the
After those are resolved it would be nice to get #34 (comment) sorted, which will likely require an issue upstream at TypeScript, some back and forth, and some lead time waiting for a release. I'll aim to keep chipping away at the remaining items. |
Co-authored-by: Remco Haszing <remcohaszing@gmail.com>
|
Here’s a diff to make the types and tests work: diff --git a/src/async.js b/src/async.js
index 64f6933..f068c71 100644
--- a/src/async.js
+++ b/src/async.js
@@ -6,9 +6,9 @@ import { WalkerBase } from './walker.js';
* @typedef {(
* this: WalkerContext,
* node: Node,
- * parent: Node,
- * key: string | number | symbol,
- * index: number
+ * parent: Node | null,
+ * key: string | number | symbol | null | undefined,
+ * index: number | null | undefined
* ) => Promise<void>} AsyncHandler
*/
@@ -47,9 +47,9 @@ export class AsyncWalker extends WalkerBase {
/**
* @template {Node} Parent
* @param {Node} node
- * @param {Parent} parent
- * @param {keyof Parent} prop
- * @param {number} index
+ * @param {Parent | null} parent
+ * @param {keyof Parent} [prop]
+ * @param {number | null} [index]
* @returns {Promise<Node | null>}
*/
async visit(node, parent, prop, index) {
@@ -84,22 +84,28 @@ export class AsyncWalker extends WalkerBase {
if (removed) return null;
}
- for (const key in node) {
- const value = node[/** @type {keyof Node} */ (key)];
-
- if (typeof value !== "object") {
- continue;
- } else if (Array.isArray(value)) {
- for (let i = 0; i < value.length; i += 1) {
- if (value[i] !== null && typeof value[i].type === 'string') {
- if (!(await this.visit(value[i], node, key, i))) {
- // removed
- i--;
+ /** @type {keyof Node} */
+ let key;
+
+ for (key in node) {
+ /** @type {unknown} */
+ const value = node[key];
+
+ if (value && typeof value === 'object') {
+ if (Array.isArray(value)) {
+ const nodes = /** @type {Array<unknown>} */ (value);
+ for (let i = 0; i < nodes.length; i += 1) {
+ const item = nodes[i];
+ if (isNode(item)) {
+ if (!(await this.visit(item, node, key, i))) {
+ // removed
+ i--;
+ }
}
}
+ } else if (isNode(value)) {
+ await this.visit(value, node, key, null);
}
- } else if (value !== null && typeof value.type === "string") {
- await this.visit(value, node, key, null);
}
}
@@ -132,3 +138,15 @@ export class AsyncWalker extends WalkerBase {
return node;
}
}
+
+/**
+ * Ducktype a node.
+ *
+ * @param {unknown} value
+ * @returns {value is Node}
+ */
+function isNode(value) {
+ return (
+ value !== null && typeof value === 'object' && 'type' in value && typeof value.type === 'string'
+ );
+}
diff --git a/src/sync.js b/src/sync.js
index a7ad898..171fb36 100644
--- a/src/sync.js
+++ b/src/sync.js
@@ -6,9 +6,9 @@ import { WalkerBase } from './walker.js';
* @typedef {(
* this: WalkerContext,
* node: Node,
- * parent: Node,
- * key?: string | number | symbol | undefined,
- * index?: number | undefined
+ * parent: Node | null,
+ * key: string | number | symbol | null | undefined,
+ * index: number | null | undefined
* ) => void} SyncHandler
*/
@@ -47,9 +47,9 @@ export class SyncWalker extends WalkerBase {
/**
* @template {Node} Parent
* @param {Node} node
- * @param {Parent} parent
- * @param {keyof Parent} prop
- * @param {number} index
+ * @param {Parent | null} parent
+ * @param {keyof Parent} [prop]
+ * @param {number | null} [index]
* @returns {Node | null}
*/
visit(node, parent, prop, index) {
@@ -84,22 +84,28 @@ export class SyncWalker extends WalkerBase {
if (removed) return null;
}
- for (const key in node) {
- const value = node[/** @type {keyof Node} */(key)];
-
- if (typeof value !== "object") {
- continue;
- } else if (Array.isArray(value)) {
- for (let i = 0; i < value.length; i += 1) {
- if (value[i] !== null && typeof value[i].type === 'string') {
- if (!this.visit(value[i], node, key, i)) {
- // removed
- i--;
+ /** @type {keyof Node} */
+ let key;
+
+ for (key in node) {
+ /** @type {unknown} */
+ const value = node[key];
+
+ if (value && typeof value === 'object') {
+ if (Array.isArray(value)) {
+ const nodes = /** @type {Array<unknown>} */ (value);
+ for (let i = 0; i < nodes.length; i += 1) {
+ const item = nodes[i];
+ if (isNode(item)) {
+ if (!this.visit(item, node, key, i)) {
+ // removed
+ i--;
+ }
}
}
+ } else if (isNode(value)) {
+ this.visit(value, node, key, null);
}
- } else if (value !== null && typeof value.type === "string") {
- this.visit(value, node, key, null);
}
}
@@ -132,3 +138,15 @@ export class SyncWalker extends WalkerBase {
return node;
}
}
+
+/**
+ * Ducktype a node.
+ *
+ * @param {unknown} value
+ * @returns {value is Node}
+ */
+function isNode(value) {
+ return (
+ value !== null && typeof value === 'object' && 'type' in value && typeof value.type === 'string'
+ );
+}
diff --git a/src/walker.js b/src/walker.js
index cfbc406..0c54bb9 100644
--- a/src/walker.js
+++ b/src/walker.js
@@ -28,31 +28,31 @@ export class WalkerBase {
/**
* @template {Node} Parent
- * @param {Parent} parent
- * @param {keyof Parent} prop
- * @param {number | null} index
+ * @param {Parent | null | undefined} parent
+ * @param {keyof Parent | null | undefined} prop
+ * @param {number | null | undefined} index
* @param {Node} node
*/
replace(parent, prop, index, node) {
- if (parent) {
+ if (parent && prop) {
if (index !== null && typeof index !== 'undefined') {
- /** @type {Array<Node>} */(parent[prop])[index] = node;
+ /** @type {Array<Node>} */ (parent[prop])[index] = node;
} else {
- /** @type {Node} */(parent[prop]) = node;
+ /** @type {Node} */ (parent[prop]) = node;
}
}
}
/**
* @template {Node} Parent
- * @param {Parent} parent
- * @param {keyof Parent} prop
- * @param {number} index
+ * @param {Parent | null | undefined} parent
+ * @param {keyof Parent | null | undefined} prop
+ * @param {number | null | undefined} index
*/
remove(parent, prop, index) {
- if (parent) {
- if (index !== null) {
- /** @type {Array<Node>} */(parent[prop]).splice(index, 1);
+ if (parent && prop) {
+ if (index !== null && index !== undefined) {
+ /** @type {Array<Node>} */ (parent[prop]).splice(index, 1);
} else {
delete parent[prop];
} |
This is an intentional decision I believe here: to allow arbitrary and even unknown nodes, while having less code, instead of tracking which nodes are valid. Otherwise you’d need a big list like https://github.com/eslint/eslint-visitor-keys/tree/main/lib. |
Co-authored-by: Titus Wormer <tituswormer@gmail.com>
Co-authored-by: Remco Haszing <remcohaszing@gmail.com>
|
@Rich-Harris this is ready for a review when you have a moment |
| return ( | ||
| value !== null && typeof value === 'object' && 'type' in value && typeof value.type === 'string' | ||
| ); | ||
| } |
There was a problem hiding this comment.
This function is defined twice. (It’s also in async.js.) I suggest to deduplicate this logic.
There was a problem hiding this comment.
a lot of the code is duplicated tho, so, yeah it’s definitely a good idea to refactor, but as this PR is already doing a lot, I’d leave that for another PR!
|
released 3.0.3. thank you! |
|
Thanks @Rich-Harris! 🙇 |
Done:
strictmode)resolves #35
resolves #28
resolves #24