From 9ebb69bb6ed3cad67ace08a19ac85c456536bd0c Mon Sep 17 00:00:00 2001 From: Lexus Drumgold Date: Mon, 4 Mar 2024 20:20:19 -0500 Subject: [PATCH] feat(visitor): `visitor` Signed-off-by: Lexus Drumgold --- .codecov.yml | 1 + .commitlintrc.ts | 3 +- README.md | 2 +- __fixtures__/.gitkeep | 0 __fixtures__/pure-comments.ts | 30 +++++ src/__tests__/visitor.functional.spec.ts | 143 +++++++++++++++++++++++ src/__tests__/visitor.spec.ts | 12 ++ src/types/__tests__/visitor.spec-d.ts | 23 ++++ src/types/index.ts | 1 + src/types/visitor.ts | 31 +++++ src/utils/__tests__/slice.spec.ts | 20 +--- src/visitor.ts | 57 +++++++++ 12 files changed, 302 insertions(+), 21 deletions(-) delete mode 100644 __fixtures__/.gitkeep create mode 100644 __fixtures__/pure-comments.ts create mode 100644 src/__tests__/visitor.functional.spec.ts create mode 100644 src/__tests__/visitor.spec.ts create mode 100644 src/types/__tests__/visitor.spec-d.ts create mode 100644 src/types/visitor.ts create mode 100644 src/visitor.ts diff --git a/.codecov.yml b/.codecov.yml index da816cc..f616602 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -90,3 +90,4 @@ ignore: profiling: critical_files_paths: - src/utils/compare.ts + - src/visitor.ts diff --git a/.commitlintrc.ts b/.commitlintrc.ts index 6ca502f..8c1312f 100644 --- a/.commitlintrc.ts +++ b/.commitlintrc.ts @@ -20,9 +20,8 @@ const config: UserConfig = { rules: { 'scope-enum': [Severity.Error, 'always', scopes([ 'chore', - 'handlers', 'util', - 'visitors' + 'visitor' ])] } } diff --git a/README.md b/README.md index 5f84844..804a9f2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![vitest](https://img.shields.io/badge/-vitest-6e9f18?style=flat&logo=vitest&logoColor=ffffff)](https://vitest.dev/) [![yarn](https://img.shields.io/badge/-yarn-2c8ebb?style=flat&logo=yarn&logoColor=ffffff)](https://yarnpkg.com/) -[esast][esast](and [estree][estree]) utility to attach comments +[esast][esast] (and [estree][estree]) utility to attach comments ## Contents diff --git a/__fixtures__/.gitkeep b/__fixtures__/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/__fixtures__/pure-comments.ts b/__fixtures__/pure-comments.ts new file mode 100644 index 0000000..aaa469a --- /dev/null +++ b/__fixtures__/pure-comments.ts @@ -0,0 +1,30 @@ +/** + * @file Fixtures - PURE_COMMENTS + * @module fixtures/PURE_COMMENTS + */ + +import type { Comment } from 'estree' + +/** + * `@__PURE__` block comments. + * + * @type {ReadonlyArray} + */ +export default Object.freeze([ + { + position: { + end: { column: 33, line: 11, offset: 411 }, + start: { column: 18, line: 11, offset: 396 } + }, + type: 'Block', + value: ' @__PURE__ ' + }, + { + position: { + end: { column: 27, line: 14, offset: 535 }, + start: { column: 12, line: 14, offset: 520 } + }, + type: 'Block', + value: ' @__PURE__ ' + } +]) diff --git a/src/__tests__/visitor.functional.spec.ts b/src/__tests__/visitor.functional.spec.ts new file mode 100644 index 0000000..8aa6537 --- /dev/null +++ b/src/__tests__/visitor.functional.spec.ts @@ -0,0 +1,143 @@ +/** + * @file Functional Tests - visitor + * @module esast-util-attach-comments/tests/functional/visitor + */ + +import PURE_COMMENTS from '#fixtures/pure-comments' +import type { State } from '#src/types' +import * as utils from '#src/utils' +import type { Spy } from '#tests/interfaces' +import type { NewExpression, Program } from 'estree' +import testSubject from '../visitor' + +describe('functional:visitor', () => { + let emptyProgram: Program + let emptyState: State + let keycheck: Spy<(typeof utils)['keycheck']> + let slice: Spy<(typeof utils)['slice']> + + beforeAll(() => { + emptyProgram = { body: [], sourceType: 'module', type: 'Program' } + emptyState = { comments: [], index: 0 } + }) + + beforeEach(() => { + keycheck = vi.spyOn(utils, 'keycheck').mockName('keycheck') + slice = vi.spyOn(utils, 'slice').mockName('slice') + }) + + describe('enter', () => { + describe('!state.comments.length', () => { + beforeEach(() => { + testSubject(emptyState)(emptyProgram, undefined) + }) + + it('should do nothing if no comments to attach', () => { + expect(keycheck).not.toHaveBeenCalled() + }) + }) + + describe('state.comments.length > 0', () => { + let node: NewExpression + let state: State + + beforeAll(() => { + state = { + comments: [...PURE_COMMENTS], + index: 1 + } + + node = { + arguments: [], + callee: { + name: 'WeakSet', + position: { + end: { column: 39, line: 14, offset: 547 }, + start: { column: 32, line: 14, offset: 540 } + }, + type: 'Identifier' + }, + position: { + end: { column: 41, line: 14, offset: 549 }, + start: { column: 28, line: 14, offset: 536 } + }, + type: 'NewExpression' + } + }) + + beforeEach(() => { + testSubject(state)(node, 'value') + }) + + it('should attach leading comments', () => { + // Arrange + const property: string = 'leadingComments' + + // Expect + expect(slice).toHaveBeenCalledOnce() + expect(slice).toHaveBeenCalledWith(state, node) + expect(node).to.have.deep.property(property, [state.comments[1]]) + }) + }) + }) + + describe('leave', () => { + let leave: true + + beforeAll(() => { + leave = true + }) + + describe('!state.comments.length', () => { + beforeEach(() => { + testSubject(emptyState, leave)(emptyProgram, undefined) + }) + + it('should do nothing if no comments to attach', () => { + expect(keycheck).not.toHaveBeenCalled() + }) + }) + + describe('state.comments.length > 0', () => { + let node: Program + let state: State + + beforeAll(() => { + state = { + comments: [ + { + position: { + end: { column: 38, line: 2, offset: 38 }, + start: { column: 1, line: 2, offset: 1 } + }, + type: 'Line', + value: '# sourceMappingURL=polyfill.mjs.map' + } + ], + index: 0 + } + + node = { + body: [], + position: { + end: { column: 1, line: 3, offset: 39 }, + start: { column: 1, line: 1, offset: 0 } + }, + sourceType: 'module', + type: 'Program' + } + }) + + beforeEach(() => { + testSubject(state)(node, undefined) + testSubject(state, leave)(node, undefined) + }) + + it('should attach comments', () => { + expect(slice).toHaveBeenCalledWith(state, node) + expect(node).to.have.deep.property('comments', state.comments) + expect(node).to.have.deep.property('trailingComments', state.comments) + }) + }) + }) +}) diff --git a/src/__tests__/visitor.spec.ts b/src/__tests__/visitor.spec.ts new file mode 100644 index 0000000..9412e4e --- /dev/null +++ b/src/__tests__/visitor.spec.ts @@ -0,0 +1,12 @@ +/** + * @file Unit Tests - visitor + * @module esast-util-attach-comments/tests/unit/visitor + */ + +import testSubject from '../visitor' + +describe('unit:visitor', () => { + it('should return visitor function', () => { + expect(testSubject({ comments: [], index: 0 })).to.be.a('function') + }) +}) diff --git a/src/types/__tests__/visitor.spec-d.ts b/src/types/__tests__/visitor.spec-d.ts new file mode 100644 index 0000000..2d9269c --- /dev/null +++ b/src/types/__tests__/visitor.spec-d.ts @@ -0,0 +1,23 @@ +/** + * @file Type Tests - Visitor + * @module esast-util-attach-comments/types/tests/unit-d/Visitor + */ + +import type { Optional } from '@flex-development/tutils' +import type { NewExpression } from 'estree' +import type { CONTINUE, EXIT } from 'estree-util-visit' +import type TestSubject from '../visitor' + +describe('unit-d:types/Visitor', () => { + it('should be callable with [T, Optional]', () => { + expectTypeOf>() + .parameters + .toEqualTypeOf<[NewExpression, Optional]>() + }) + + it('should return typeof CONTINUE | typeof EXIT', () => { + expectTypeOf() + .returns + .toEqualTypeOf() + }) +}) diff --git a/src/types/index.ts b/src/types/index.ts index 82dadeb..404cf5a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,3 +4,4 @@ */ export type { default as State } from './state' +export type { default as Visitor } from './visitor' diff --git a/src/types/visitor.ts b/src/types/visitor.ts new file mode 100644 index 0000000..3924e29 --- /dev/null +++ b/src/types/visitor.ts @@ -0,0 +1,31 @@ +/** + * @file Type Definitions - Visitor + * @module esast-util-attach-comments/types/Visitor + */ + +import type { Optional } from '@flex-development/tutils' +import type { Node } from 'estree' +import type { CONTINUE, EXIT } from 'estree-util-visit' + +/** + * Attach comments when entering or leaving `node`. + * + * @see {@linkcode CONTINUE} + * @see {@linkcode EXIT} + * @see {@linkcode Node} + * + * @internal + * + * @template {Node} [T=Node] - Node type + * + * @param {T} node - Node being entered or exited + * @param {Optional} key - Field at which `node` lives in its parent + * (or where a list of nodes live if `parent[key]` is an array) + * @return {typeof CONTINUE | typeof EXIT} Next action + */ +type Visitor = ( + node: T, + key: Optional +) => typeof CONTINUE | typeof EXIT + +export type { Visitor as default } diff --git a/src/utils/__tests__/slice.spec.ts b/src/utils/__tests__/slice.spec.ts index e3f9ba5..e07e750 100644 --- a/src/utils/__tests__/slice.spec.ts +++ b/src/utils/__tests__/slice.spec.ts @@ -3,6 +3,7 @@ * @module esast-util-attach-comments/utils/tests/unit/slice */ +import comments from '#fixtures/pure-comments' import type { State } from '#src/types' import type { NewExpression } from 'estree' import testSubject from '../slice' @@ -13,24 +14,7 @@ describe('unit:utils/slice', () => { beforeAll(() => { state = { - comments: [ - { - position: { - end: { column: 33, line: 11, offset: 411 }, - start: { column: 18, line: 11, offset: 396 } - }, - type: 'Block', - value: ' @__PURE__ ' - }, - { - position: { - end: { column: 27, line: 14, offset: 535 }, - start: { column: 12, line: 14, offset: 520 } - }, - type: 'Block', - value: ' @__PURE__ ' - } - ], + comments: [...comments], index: 0 } diff --git a/src/visitor.ts b/src/visitor.ts new file mode 100644 index 0000000..6feed33 --- /dev/null +++ b/src/visitor.ts @@ -0,0 +1,57 @@ +/** + * @file visitor + * @module esast-util-attach-comments/visitor + */ + +import type { Optional } from '@flex-development/tutils' +import type { Node } from 'estree' +import { CONTINUE, EXIT } from 'estree-util-visit' +import type { State, Visitor } from './types' +import { keycheck, slice } from './utils' + +declare module 'estree' { + interface BaseNode { + comments?: Comment[] | undefined + } +} + +/** + * Create a visitor to attach comments when entering or leaving a node. + * + * @see {@linkcode State} + * @see {@linkcode Visitor} + * + * @param {State} state - Visitor state + * @param {boolean?} [leave] - Visiting nodes on exit? + * @return {Visitor} Visitor function + */ +function visitor(state: State, leave?: boolean): Visitor { + /** + * Attach comments when entering or leaving `node`. + * + * @param {Node} node - Node being entered or exited + * @param {Optional} key - Field at which `node` lives in its parent + * (or where a list of nodes live if `parent[key]` is an array) + * @return {typeof CONTINUE | typeof EXIT} Next action + */ + return (node: Node, key: Optional): typeof CONTINUE | typeof EXIT => { + if (!state.comments.length) return EXIT + + if (keycheck(key)) { + if ((state.leave = !!leave)) { + node.comments = [] + node.trailingComments = [] + node.trailingComments.push(...slice(state, node)) + node.comments.push(...node.leadingComments!) + node.comments.push(...node.trailingComments) + } else { + node.leadingComments = [] + node.leadingComments.push(...slice(state, node)) + } + } + + return CONTINUE + } +} + +export default visitor