Skip to content

Commit

Permalink
♻️ Improve AST node type guards
Browse files Browse the repository at this point in the history
Renamed the utility type `NodeIsHelper` to
`PotentiallyReadonly` and improved documentation
and tests.
  • Loading branch information
SPGoding committed May 11, 2024
1 parent f0d8613 commit ffa71af
Show file tree
Hide file tree
Showing 7 changed files with 55 additions and 23 deletions.
8 changes: 6 additions & 2 deletions packages/core/src/common/ReadonlyProxy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { emplaceMap, isObject } from './util.js'

type Wrap<T> = T extends object ? DeepReadonly<T> : T
type DeepReadonlyValue<T> = T extends object ? DeepReadonly<T> : T
export type DeepReadonly<T extends object> = {
readonly [K in keyof T]: Wrap<T[K]>
readonly [K in keyof T]: DeepReadonlyValue<T[K]>
}

export type ReadWrite<T extends object> = {
-readonly [K in keyof T]: T[K]
}

export const ReadonlyProxy = Object.freeze({
Expand Down
34 changes: 17 additions & 17 deletions packages/core/src/common/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import rfdc from 'rfdc'
import type { AstNode } from '../node/index.js'
import type { ProcessorContext } from '../service/index.js'
import type { Externals } from './externals/index.js'
import type { DeepReadonly } from './ReadonlyProxy.js'
import type { DeepReadonly, ReadWrite } from './ReadonlyProxy.js'

export const Uri = URL
export type Uri = URL
Expand Down Expand Up @@ -335,25 +335,25 @@ export function normalizeUri(uri: string): string {
}

/**
* Return a read-write TARGET type if the INPUT type is read-write, and a
* readonly TARGET type if the INPUT type is readonly, and `never` if the INPUT
* type is `undefined`.
*
* It is used in the return type of an AST node
* [user-defined type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates).
*
* @example
* ```ts
* function isCommentNode<T extends DeepReadonly<AstNode> | undefined>(node: T): node is IsHelper<AstNode, CommentNode, T>
* ```
*/
export type IsHelper<
ROOT extends object,
TARGET extends ROOT,
INPUT extends DeepReadonly<ROOT> | undefined,
> = INPUT extends DeepReadonly<ROOT> ? INPUT & DeepReadonly<TARGET>
: INPUT & TARGET

/**
* @example
* ```ts
* function isCommentNode<T extends DeepReadonly<AstNode> | undefined>(node: T): node is NodeIsHelper<CommentNode, T>
* export const CommentNode = Object.freeze({
* is<T extends DeepReadonly<AstNode> | undefined>(
* obj: T,
* ): obj is PotentiallyReadonly<CommentNode, T> {
* return (obj as CommentNode | undefined)?.type === 'comment'
* },
* })
* ```
*/
export type NodeIsHelper<
export type PotentiallyReadonly<
TARGET extends AstNode,
INPUT extends DeepReadonly<AstNode> | undefined,
> = IsHelper<AstNode, TARGET, INPUT>
> = INPUT & (INPUT extends ReadWrite<AstNode> ? TARGET : DeepReadonly<TARGET>)
8 changes: 6 additions & 2 deletions packages/core/src/node/CommentNode.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { DeepReadonly, NodeIsHelper } from '../common/index.js'
import type {
DeepReadonly,
PotentiallyReadonly,
ReadWrite,
} from '../common/index.js'
import type { AstNode } from './AstNode.js'

export interface CommentNode extends AstNode {
Expand All @@ -12,7 +16,7 @@ export interface CommentNode extends AstNode {
export const CommentNode = Object.freeze({
is<T extends DeepReadonly<AstNode> | undefined>(
obj: T,
): obj is NodeIsHelper<CommentNode, T> {
): obj is PotentiallyReadonly<CommentNode, T> {
return (obj as CommentNode | undefined)?.type === 'comment'
},
})
22 changes: 22 additions & 0 deletions packages/core/test/common/util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, it } from 'mocha'
import type {
AstNode,
CommentNode,
DeepReadonly,
PotentiallyReadonly,
} from '../../lib'
import { assertType, typing } from '../utils.js'

describe('common util', () => {
typing('PotentiallyReadonly', () => {
type UndefinedNode = PotentiallyReadonly<CommentNode, undefined>
type ReadonlyNode = PotentiallyReadonly<
CommentNode,
DeepReadonly<AstNode>
>
type ReadWriteNode = PotentiallyReadonly<CommentNode, AstNode>
assertType<never>(0 as unknown as UndefinedNode)
assertType<DeepReadonly<CommentNode>>(0 as unknown as ReadonlyNode)
assertType<CommentNode>(0 as unknown as ReadWriteNode)
})
})
2 changes: 2 additions & 0 deletions packages/core/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ export function testParser(
export function typing(_title: string, _fn: () => void): void {}

/**
* Assert the type of `_value` is `T`.
*
* This function should never be actually executed at runtime.
* Enclose it inside the body of a {@link typing} function.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/java-edition/src/mcfunction/node/argument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export namespace EntitySelectorNode {
/* istanbul ignore next */
export function is<T extends core.DeepReadonly<core.AstNode> | undefined>(
node: T,
): node is core.NodeIsHelper<EntitySelectorNode, T> {
): node is core.PotentiallyReadonly<EntitySelectorNode, T> {
return (
(node as EntitySelectorNode | undefined)?.type ===
'mcfunction:entity_selector'
Expand Down
2 changes: 1 addition & 1 deletion packages/mcfunction/src/node/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface CommandMacroNode extends core.AstNode {
export const CommandMacroNode = Object.freeze({
is<T extends core.DeepReadonly<core.AstNode> | undefined>(
obj: T,
): obj is core.NodeIsHelper<CommandMacroNode, T> {
): obj is core.PotentiallyReadonly<CommandMacroNode, T> {
return (obj as CommandMacroNode | undefined)?.type ===
'mcfunction:command_macro'
},
Expand Down

0 comments on commit ffa71af

Please sign in to comment.