From 926cc37f58ab3549f8d7be3403a08f0df05155c7 Mon Sep 17 00:00:00 2001 From: Lexus Drumgold Date: Mon, 4 Mar 2024 22:31:49 -0500 Subject: [PATCH] feat(util): `attachComments` Signed-off-by: Lexus Drumgold --- .codecov.yml | 1 + .dictionary.txt | 2 + __fixtures__/gemoji-shortcode.mjs | 134 ++++++++++++++++++++++++ __fixtures__/micromark-util-types.d.mts | 7 ++ package.json | 5 + src/__snapshots__/util.integration.snap | 105 +++++++++++++++++++ src/__tests__/util.integration.spec.ts | 54 ++++++++++ src/index.ts | 2 +- src/util.ts | 45 ++++++++ src/visitor.ts | 14 ++- yarn.lock | 15 ++- 11 files changed, 377 insertions(+), 7 deletions(-) create mode 100644 __fixtures__/gemoji-shortcode.mjs create mode 100644 __fixtures__/micromark-util-types.d.mts create mode 100644 src/__snapshots__/util.integration.snap create mode 100644 src/__tests__/util.integration.spec.ts create mode 100644 src/util.ts diff --git a/.codecov.yml b/.codecov.yml index f616602..0e8674e 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -90,4 +90,5 @@ ignore: profiling: critical_files_paths: - src/utils/compare.ts + - src/util.ts - src/visitor.ts diff --git a/.dictionary.txt b/.dictionary.txt index 9346237..561343f 100644 --- a/.dictionary.txt +++ b/.dictionary.txt @@ -7,12 +7,14 @@ dedupe dedupe deno dessant +devlop docast dohm dprint esast estree fbca +gemoji ggshield gpgsign hmarr diff --git a/__fixtures__/gemoji-shortcode.mjs b/__fixtures__/gemoji-shortcode.mjs new file mode 100644 index 0000000..0486352 --- /dev/null +++ b/__fixtures__/gemoji-shortcode.mjs @@ -0,0 +1,134 @@ +/** + * @file Fixtures - gemojiShortcode + * @module fixtures/gemojiShortcode + */ + +import { ok as assert } from 'devlop' +import { asciiAlphanumeric } from 'micromark-util-character' +import { codes } from 'micromark-util-symbol' + +/** + * Construct a union of `T` and `undefined`. + * + * @template T + * @typedef {import('@flex-development/tutils').Optional} Optional + */ + +/** + * @typedef {import('micromark-util-types').Code} Code + * @typedef {import('micromark-util-types').Construct} Construct + * @typedef {import('micromark-util-types').Effects} Effects + * @typedef {import('micromark-util-types').State} State + * @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext + */ + +/** + * Guard whether `code` can come before a gemoji. + * + * @see {@linkcode Code} + * + * @this {TokenizeContext} + * + * @param {Code} code - Previous character code + * @return {boolean} `true` if `code` allowed before construct + */ +function previous(code) { + return code !== codes.backslash && code !== codes.colon +} + +/** + * Gemoji (`:+1:`) construct. + * + * @type {Construct} + */ +export default { + /** + * Construct name. + */ + name: 'gemoji', + + /** + * Guard whether `code` can come before this construct. + * + * @see {@linkcode Code} + * + * @this {TokenizeContext} + * + * @param {Code} code - Previous character code + * @return {boolean} `true` if `code` allowed before construct + */ + previous, + + /** + * Set up a state machine to process character codes. + * + * @see {@linkcode Code} + * @see {@linkcode Effects} + * @see {@linkcode State} + * + * @this {TokenizeContext} + * + * @param {Effects} effects - Context object to transition state machine + * @param {State} ok - Success state function + * @param {State} nok - Error state function + * @return {State} Initial state + */ + tokenize(effects, ok, nok) { + /** + * Process the inside (`+1`) and end (`:`) of a gemoji shortcode. + * + * @param {Code} code - Character code to process + * @return {Optional} Next state + */ + function inside(code) { + switch (true) { + case code === codes.colon: + effects.consume(code) + effects.exit('gemoji') + return ok + case asciiAlphanumeric(code): + case code === codes.dash: + case code === codes.plusSign: + case code === codes.underscore: + effects.consume(code) + return inside + default: + return nok(code) + } + } + + /** + * Begin processing a gemoji shortcode. + * + * @param {Code} code - Character code to process + * @return {Optional} Next state + */ + function begin(code) { + switch (code) { + case codes.eof: + case codes.colon: + return nok(code) + default: + effects.consume(code) + return inside + } + } + + /** + * Process the start of a gemoji shortcode (`:`). + * + * @param {Code} code - Character code to process + * @return {State} Next state + */ + const start = code => { + assert(code === codes.colon, 'expected `:`') + assert(previous.call(this, this.previous), 'expected correct previous') + effects.enter('gemoji') + effects.consume(code) + return begin + } + + return start + } +} +// # sourceMappingURL=gemoji-shortcode.mjs.map diff --git a/__fixtures__/micromark-util-types.d.mts b/__fixtures__/micromark-util-types.d.mts new file mode 100644 index 0000000..374100e --- /dev/null +++ b/__fixtures__/micromark-util-types.d.mts @@ -0,0 +1,7 @@ +declare module 'micromark-util-types' { + interface TokenTypeMap { + gemoji: 'gemoji' + } +} + +export {} diff --git a/package.json b/package.json index 8280cc8..6de22c7 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "chai": "5.1.0", "cross-env": "7.0.3", "cspell": "8.5.0", + "devlop": "1.1.0", "dprint": "0.45.0", "editorconfig": "2.0.0", "esast-util-from-js": "2.0.1", @@ -131,6 +132,9 @@ "is-ci": "3.0.1", "jsonc-eslint-parser": "2.4.0", "lint-staged": "15.2.2", + "micromark-util-character": "2.1.0", + "micromark-util-symbol": "2.0.0", + "micromark-util-types": "2.0.0", "node-notifier": "10.0.1", "prettier": "3.2.5", "remark": "15.0.1", @@ -142,6 +146,7 @@ "trash-cli": "5.0.0", "ts-dedent": "2.2.0", "typescript": "5.3.3", + "unist-util-inspect": "8.0.0", "vfile": "6.0.1", "vite-tsconfig-paths": "4.3.1", "vitest": "1.3.1", diff --git a/src/__snapshots__/util.integration.snap b/src/__snapshots__/util.integration.snap new file mode 100644 index 0000000..d961f42 --- /dev/null +++ b/src/__snapshots__/util.integration.snap @@ -0,0 +1,105 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`integration:util > should attach comments 1`] = ` +Program + body: + ├─0 ImportDeclaration + │ specifiers: + │ └─0 ImportSpecifier + │ imported: Identifier + │ local: Identifier + │ source: Literal "devlop" + ├─1 ImportDeclaration + │ specifiers: + │ └─0 ImportSpecifier + │ imported: Identifier + │ local: Identifier + │ source: Literal "micromark-util-character" + ├─2 ImportDeclaration + │ specifiers: + │ └─0 ImportSpecifier + │ imported: Identifier + │ local: Identifier + │ source: Literal "micromark-util-symbol" + ├─3 FunctionDeclaration + │ id: Identifier + │ expression: false + │ generator: false + │ async: false + │ params: + │ └─0 Identifier + │ body: BlockStatement + │ body: + │ └─0 ReturnStatement + │ argument: LogicalExpression + │ left: BinaryExpression + │ left: Identifier + │ operator: "!==" + │ right: MemberExpression + │ object: Identifier + │ property: Identifier + │ computed: false + │ optional: false + │ operator: "&&" + │ right: BinaryExpression + │ left: Identifier + │ operator: "!==" + │ right: MemberExpression + │ object: Identifier + │ property: Identifier + │ computed: false + │ optional: false + │ leadingComments: + │ ├─0 Block "*\\n * Construct a union of \`T\` and \`undefined\`.\\n *\\n * @template T\\n * @typedef {import('@flex-development/tutils').Optional} Optional\\n " + │ ├─1 Block "*\\n * @typedef {import('micromark-util-types').Code} Code\\n * @typedef {import('micromark-util-types').Construct} Construct\\n * @typedef {import('micromark-util-types').Effects} Effects\\n * @typedef {import('micromark-util-types').State} State\\n * @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext\\n " + │ └─2 Block "*\\n * Guard whether \`code\` can come before a gemoji.\\n *\\n * @see {@linkcode Code}\\n *\\n * @this {TokenizeContext}\\n *\\n * @param {Code} code - Previous character code\\n * @return {boolean} \`true\` if \`code\` allowed before construct\\n " + │ comments: + │ ├─0 Block "*\\n * Construct a union of \`T\` and \`undefined\`.\\n *\\n * @template T\\n * @typedef {import('@flex-development/tutils').Optional} Optional\\n " + │ ├─1 Block "*\\n * @typedef {import('micromark-util-types').Code} Code\\n * @typedef {import('micromark-util-types').Construct} Construct\\n * @typedef {import('micromark-util-types').Effects} Effects\\n * @typedef {import('micromark-util-types').State} State\\n * @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext\\n " + │ └─2 Block "*\\n * Guard whether \`code\` can come before a gemoji.\\n *\\n * @see {@linkcode Code}\\n *\\n * @this {TokenizeContext}\\n *\\n * @param {Code} code - Previous character code\\n * @return {boolean} \`true\` if \`code\` allowed before construct\\n " + └─4 ExportDefaultDeclaration + declaration: ObjectExpression + properties: + ├─0 Property + │ method: false + │ shorthand: false + │ computed: false + │ key: Identifier + │ kind: "init" + │ leadingComments: + │ └─0 Block "*\\n * Construct name.\\n " + │ comments: + │ └─0 Block "*\\n * Construct name.\\n " + ├─1 Property + │ method: false + │ shorthand: true + │ computed: false + │ key: Identifier + │ kind: "init" + │ leadingComments: + │ └─0 Block "*\\n * Guard whether \`code\` can come before this construct.\\n *\\n * @see {@linkcode Code}\\n *\\n * @this {TokenizeContext}\\n *\\n * @param {Code} code - Previous character code\\n * @return {boolean} \`true\` if \`code\` allowed before construct\\n " + │ comments: + │ └─0 Block "*\\n * Guard whether \`code\` can come before this construct.\\n *\\n * @see {@linkcode Code}\\n *\\n * @this {TokenizeContext}\\n *\\n * @param {Code} code - Previous character code\\n * @return {boolean} \`true\` if \`code\` allowed before construct\\n " + └─2 Property + method: true + shorthand: false + computed: false + key: Identifier + kind: "init" + leadingComments: + └─0 Block "*\\n * Set up a state machine to process character codes.\\n *\\n * @see {@linkcode Code}\\n * @see {@linkcode Effects}\\n * @see {@linkcode State}\\n *\\n * @this {TokenizeContext}\\n *\\n * @param {Effects} effects - Context object to transition state machine\\n * @param {State} ok - Success state function\\n * @param {State} nok - Error state function\\n * @return {State} Initial state\\n " + comments: + └─0 Block "*\\n * Set up a state machine to process character codes.\\n *\\n * @see {@linkcode Code}\\n * @see {@linkcode Effects}\\n * @see {@linkcode State}\\n *\\n * @this {TokenizeContext}\\n *\\n * @param {Effects} effects - Context object to transition state machine\\n * @param {State} ok - Success state function\\n * @param {State} nok - Error state function\\n * @return {State} Initial state\\n " + leadingComments: + └─0 Block "*\\n * Gemoji (\`:+1:\`) construct.\\n *\\n * @type {Construct}\\n " + comments: + └─0 Block "*\\n * Gemoji (\`:+1:\`) construct.\\n *\\n * @type {Construct}\\n " + sourceType: "module" + comments: + ├─0 Block "*\\n * @file Fixtures - gemojiShortcode\\n * @module fixtures/gemojiShortcode\\n " + └─1 Line " # sourceMappingURL=gemoji-shortcode.mjs.map" + leadingComments: + └─0 Block "*\\n * @file Fixtures - gemojiShortcode\\n * @module fixtures/gemojiShortcode\\n " + trailingComments: + └─0 Line " # sourceMappingURL=gemoji-shortcode.mjs.map" +`; diff --git a/src/__tests__/util.integration.spec.ts b/src/__tests__/util.integration.spec.ts new file mode 100644 index 0000000..4a4b024 --- /dev/null +++ b/src/__tests__/util.integration.spec.ts @@ -0,0 +1,54 @@ +/** + * @file Integration Tests - util + * @module esast-util-attach-comments/tests/integration/util + */ + +import { constant } from '@flex-development/tutils' +import { fromJs } from 'esast-util-from-js' +import type { Program } from 'estree' +import { visit } from 'estree-util-visit' +import { read } from 'to-vfile' +import { inspectNoColor as inspect } from 'unist-util-inspect' +import type { VFile } from 'vfile' +import type { TestContext } from 'vitest' +import testSubject from '../util' + +describe('integration:util', () => { + let file: VFile + let tree: Program + + beforeAll(async () => { + file = await read('__fixtures__/gemoji-shortcode.mjs') + }) + + beforeEach((ctx: TestContext): void => { + ctx.expect.addSnapshotSerializer({ + print: (val: unknown): string => inspect(val, { showPositions: false }), + test: constant(true) + }) + + tree = fromJs(String(file), { module: true }) + }) + + it('should attach comments', () => { + // Act + testSubject(tree, tree.comments) + + // Expect + expect(tree).toMatchSnapshot() + }) + + it('should handle empty comments list', () => { + // Act + testSubject(tree) + + // Expect + visit(tree, node => { + if (node.type !== 'Program') { + expect(node).not.to.have.property('comments') + expect(node).not.to.have.property('leadingComments') + expect(node).not.to.have.property('trailingComments') + } + }) + }) +}) diff --git a/src/index.ts b/src/index.ts index 99aec1f..93fecfa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,4 +3,4 @@ * @module esast-util-attach-comments */ -export {} +export { default as attachComments } from './util' diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..d48dfde --- /dev/null +++ b/src/util.ts @@ -0,0 +1,45 @@ +/** + * @file attachComments + * @module esast-util-attach-comments/util + */ + +import type { Nilable } from '@flex-development/tutils' +import type { Comment, Node } from 'estree' +import { visit } from 'estree-util-visit' +import type { State } from './types' +import { compare } from './utils' +import visitor from './visitor' + +/** + * Attach comment nodes. + * + * @template {Node} [T=Node] - Node type + * + * @this {void} + * + * @param {T} tree - Tree to attach comments to + * @param {Nilable?} [comments] - List of comments + * @return {void} Nothing + */ +function attachComments( + this: void, + tree: T, + comments?: Nilable +): void { + /** + * Visitor state. + * + * @const {State} state + */ + const state: State = { + comments: [...(comments ?? [])].sort(compare), + index: 0 + } + + return void visit(tree, { + enter: visitor(state), + leave: visitor(state, true) + }) +} + +export default attachComments diff --git a/src/visitor.ts b/src/visitor.ts index 6feed33..531209f 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -4,7 +4,7 @@ */ import type { Optional } from '@flex-development/tutils' -import type { Node } from 'estree' +import type { Comment, Node } from 'estree' import { CONTINUE, EXIT } from 'estree-util-visit' import type { State, Visitor } from './types' import { keycheck, slice } from './utils' @@ -44,6 +44,18 @@ function visitor(state: State, leave?: boolean): Visitor { node.trailingComments.push(...slice(state, node)) node.comments.push(...node.leadingComments!) node.comments.push(...node.trailingComments) + + for (const [key, value] of Object.entries(node)) { + switch (key) { + case 'comments': + case 'leadingComments': + case 'trailingComments': + !(value).length && delete node[key] + break + default: + break + } + } } else { node.leadingComments = [] node.leadingComments.push(...slice(state, node)) diff --git a/yarn.lock b/yarn.lock index 6fbd6bf..25f9043 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1464,6 +1464,7 @@ __metadata: chai: "npm:5.1.0" cross-env: "npm:7.0.3" cspell: "npm:8.5.0" + devlop: "npm:1.1.0" dprint: "npm:0.45.0" editorconfig: "npm:2.0.0" esast-util-from-js: "npm:2.0.1" @@ -1490,6 +1491,9 @@ __metadata: is-ci: "npm:3.0.1" jsonc-eslint-parser: "npm:2.4.0" lint-staged: "npm:15.2.2" + micromark-util-character: "npm:2.1.0" + micromark-util-symbol: "npm:2.0.0" + micromark-util-types: "npm:2.0.0" node-notifier: "npm:10.0.1" prettier: "npm:3.2.5" remark: "npm:15.0.1" @@ -1501,6 +1505,7 @@ __metadata: trash-cli: "npm:5.0.0" ts-dedent: "npm:2.2.0" typescript: "npm:5.3.3" + unist-util-inspect: "npm:8.0.0" vfile: "npm:6.0.1" vite-tsconfig-paths: "npm:4.3.1" vitest: "npm:1.3.1" @@ -3991,7 +3996,7 @@ __metadata: languageName: node linkType: hard -"devlop@npm:^1.0.0, devlop@npm:^1.1.0": +"devlop@npm:1.1.0, devlop@npm:^1.0.0, devlop@npm:^1.1.0": version: 1.1.0 resolution: "devlop@npm:1.1.0" dependencies: @@ -7587,7 +7592,7 @@ __metadata: languageName: node linkType: hard -"micromark-util-character@npm:^2.0.0": +"micromark-util-character@npm:2.1.0, micromark-util-character@npm:^2.0.0": version: 2.1.0 resolution: "micromark-util-character@npm:2.1.0" dependencies: @@ -7719,14 +7724,14 @@ __metadata: languageName: node linkType: hard -"micromark-util-symbol@npm:^2.0.0": +"micromark-util-symbol@npm:2.0.0, micromark-util-symbol@npm:^2.0.0": version: 2.0.0 resolution: "micromark-util-symbol@npm:2.0.0" checksum: 10/8c662644c326b384f02a5269974d843d400930cf6f5d6a8e6db1743fc8933f5ecc125b4203ad4ebca25447f5d23eb7e5bf1f75af34570c3fdd925cb618752fcd languageName: node linkType: hard -"micromark-util-types@npm:^2.0.0": +"micromark-util-types@npm:2.0.0, micromark-util-types@npm:^2.0.0": version: 2.0.0 resolution: "micromark-util-types@npm:2.0.0" checksum: 10/b88e0eefd4b7c8d86b54dbf4ed0094ef56a3b0c7774d040bd5c8146b8e4e05b1026bbf1cd9308c8fcd05ecdc0784507680c8cee9888a4d3c550e6e574f7aef62 @@ -10201,7 +10206,7 @@ __metadata: languageName: node linkType: hard -"unist-util-inspect@npm:^8.0.0": +"unist-util-inspect@npm:8.0.0, unist-util-inspect@npm:^8.0.0": version: 8.0.0 resolution: "unist-util-inspect@npm:8.0.0" dependencies: