From 2263e70bfe7370c1d1b4894f9cf22cb855a7ecc9 Mon Sep 17 00:00:00 2001 From: Jarrod Overson Date: Mon, 6 Jul 2020 22:58:38 -0400 Subject: [PATCH] ironing out the wrinkles --- src/id-generator.ts | 69 ++---- src/index.ts | 32 +-- src/refactor-plugin-common.ts | 30 +-- src/refactor-plugin-unsafe.ts | 20 +- src/refactor-session-chainable.ts | 257 ++++++++++++++++------ src/refactor-session.ts | 161 ++------------ src/types.ts | 2 + src/util.ts | 14 +- test/doclint/examples.ts | 45 ++-- test/id-generator.test.ts | 29 +-- test/plugin-unsafe/pure-functions.test.ts | 160 +++++++------- test/refactor-chainable-api.test.ts | 18 +- test/refactor-session.test.ts | 16 +- test/regression.test.ts | 4 +- 14 files changed, 415 insertions(+), 442 deletions(-) diff --git a/src/id-generator.ts b/src/id-generator.ts index cf92f8a..e07e6d2 100644 --- a/src/id-generator.ts +++ b/src/id-generator.ts @@ -1,4 +1,4 @@ -const {TokenType} = require('shift-parser'); +const { TokenType } = require('shift-parser'); const jsKeywords = Object.values(TokenType) .filter((_: any) => _.name && _.klass.name === 'Keyword') @@ -9,14 +9,26 @@ import adjectives from './adjectives'; import seedrandom from 'seedrandom'; -export interface IdGenerator extends Iterator { - next(): IteratorResult; +export class BaseIdGenerator implements Iterator { + next() { + return { + done: false, + value: ``, + }; + } + + *[Symbol.iterator]() { + while (true) { + yield this.next(); + } + } } -export class MemorableIdGenerator implements IdGenerator { +export class MemorableIdGenerator extends BaseIdGenerator { rng: seedrandom.prng; constructor(seed = 0) { + super(); this.rng = seedrandom(seed.toString()); } @@ -45,52 +57,3 @@ export class MemorableIdGenerator implements IdGenerator { } } } - -export class BasicIdGenerator implements IdGenerator { - alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - reservedWords: Set = new Set(jsKeywords); - current: number[]; - - constructor(alphabet: string, reservedWords?: string[]) { - if (alphabet) this.alphabet = alphabet; - if (reservedWords) this.reservedWords = new Set(reservedWords); - this.current = [-1]; - } - - next() { - this._increment(); - const nextId = this.current.reduce((acc, code) => acc + this.alphabet[code], ''); - if (!this.reservedWords.has(nextId)) { - return { - done: false, - value: nextId, - }; - } else { - this._increment(); - return { - done: false, - value: this.current.reduce((acc, code) => acc + this.alphabet[code], ''), - }; - } - } - - _increment() { - for (let i = this.current.length - 1; i >= 0; i--) { - this.current[i]++; - if (this.current[i] >= this.alphabet.length) { - this.current[i] = 0; - } else { - // if we didn't have to roll over, then return - return; - } - } - // if we rolled over every character, add one more. - this.current.unshift(0); - } - - *[Symbol.iterator]() { - while (true) { - yield this.next(); - } - } -} diff --git a/src/index.ts b/src/index.ts index 4c93d4e..b3f8dd7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,34 +5,8 @@ import pluginUnsafe from './refactor-plugin-unsafe'; import pluginCommon from './refactor-plugin-common'; export { RefactorSession } from './refactor-session'; +export { RefactorSessionChainable } from './refactor-session-chainable' -const API = RefactorSessionChainable.with(pluginUnsafe).with(pluginCommon); - -/** - * Initialization of a RefactorSession via the chainable API - * - * @alpha - */ -export function refactor(input: string | Node, { autoCleanup = true } = {}) { - const globalSession = new RefactorSession(input, { autoCleanup }); - const globalChainable = new API(globalSession); - - function generateQueryFunction(session: RefactorSession) { - return function $query(selector: string | string[]) { - const subSession = session.subSession(selector); - const subChainable = new API(subSession); - const prototype = Object.getPrototypeOf(subChainable); - const hybridObject = Object.assign(generateQueryFunction(subSession), subChainable); - Object.setPrototypeOf(hybridObject, prototype); - Object.defineProperty(hybridObject, 'length', { - get() { return subSession.length } - }); - return hybridObject; - } - } - const prototype = Object.getPrototypeOf(globalChainable); - const hybridObject = Object.assign(generateQueryFunction(globalSession), globalChainable); - Object.setPrototypeOf(hybridObject, prototype); - return hybridObject; -} +// export * as Shift from 'shift-ast'; +export { refactor } from './refactor-session-chainable'; \ No newline at end of file diff --git a/src/refactor-plugin-common.ts b/src/refactor-plugin-common.ts index a11a9e4..2d5ce6e 100644 --- a/src/refactor-plugin-common.ts +++ b/src/refactor-plugin-common.ts @@ -1,29 +1,21 @@ +import DEBUG from 'debug'; import { - ComputedMemberAssignmentTarget, + BinaryExpression, ComputedMemberAssignmentTarget, ComputedMemberExpression, ComputedPropertyName, - IdentifierExpression, + ConditionalExpression, + DebuggerStatement, FunctionBody, IdentifierExpression, LiteralBooleanExpression, - LiteralStringExpression, - StaticMemberAssignmentTarget, - StaticMemberExpression, - StaticPropertyName, - VariableDeclarator, Node, - BinaryExpression, - ConditionalExpression, - FunctionBody, - DebuggerStatement, - ReturnStatement, + ReturnStatement, StaticMemberAssignmentTarget, + StaticMemberExpression, + StaticPropertyName } from 'shift-ast'; import { Declaration, Reference } from 'shift-scope'; import { default as isValid } from 'shift-validator'; -import { IdGenerator, MemorableIdGenerator } from './id-generator'; -import { RefactorPlugin } from './refactor-plugin'; -import { SelectorOrNode } from './types'; -import { findNodes, renameScope, isLiteral } from './util'; -import DEBUG from 'debug'; +import { MemorableIdGenerator } from './id-generator'; import { RefactorSessionChainable } from './refactor-session-chainable'; +import { isLiteral, renameScope } from './util'; const debug = DEBUG('shift-refactor:common'); @@ -168,9 +160,9 @@ export default function pluginCommon() { return this.session.conditionalCleanup(); }, - normalizeIdentifiers(this: RefactorSessionChainable, seed = 1, _Generator: new (seed: number) => IdGenerator = MemorableIdGenerator) { + normalizeIdentifiers(this: RefactorSessionChainable, seed = 1) { const lookupTable = this.session.getLookupTable(); - const idGenerator = new _Generator(seed); + const idGenerator = new MemorableIdGenerator(seed); renameScope(lookupTable.scope, idGenerator, this.session.parentMap); return this.session.conditionalCleanup(); } diff --git a/src/refactor-plugin-unsafe.ts b/src/refactor-plugin-unsafe.ts index 010c5b5..d04a178 100644 --- a/src/refactor-plugin-unsafe.ts +++ b/src/refactor-plugin-unsafe.ts @@ -6,14 +6,14 @@ import { copy, isLiteral, isStatement } from './util'; export default function pluginUnsafe() { return { - findPureFunctionCandidates(this: RefactorSessionChainable, options?: PureFunctionAssessmentOptions) { - return new Map( - (this.query('FunctionDeclaration') as FunctionDeclaration[]) - .map((fn: FunctionDeclaration) => new PureFunctionAssessment(fn, options)) - .filter((assmt: PureFunctionAssessment) => assmt.verdict === PureFunctionVerdict.Probably) - .map((assmt: PureFunctionAssessment) => [assmt.node, assmt]), - ); - }, + // findPureFunctionCandidates(this: RefactorSessionChainable, options?: PureFunctionAssessmentOptions) { + // return new Map( + // (this.query('FunctionDeclaration') as FunctionDeclaration[]) + // .map((fn: FunctionDeclaration) => new PureFunctionAssessment(fn, options)) + // .filter((assmt: PureFunctionAssessment) => assmt.verdict === PureFunctionVerdict.Probably) + // .map((assmt: PureFunctionAssessment) => [assmt.node, assmt]), + // ); + // }, massRename(this: RefactorSessionChainable, namePairs: string[][]) { namePairs.forEach(([from, to]) => { @@ -47,8 +47,8 @@ export default function pluginUnsafe() { }, removeDeadVariables(this: RefactorSessionChainable) { - (this - .query('VariableDeclarator, FunctionDeclaration, ClassDeclaration') as (VariableDeclarator | FunctionDeclaration | ClassDeclaration)[]) + this + .query('VariableDeclarator, FunctionDeclaration, ClassDeclaration') .forEach((decl: VariableDeclarator | FunctionDeclaration | ClassDeclaration) => { let nameNode = decl.type === 'VariableDeclarator' ? decl.binding : decl.name; diff --git a/src/refactor-session-chainable.ts b/src/refactor-session-chainable.ts index 431821f..84c3a7c 100644 --- a/src/refactor-session-chainable.ts +++ b/src/refactor-session-chainable.ts @@ -1,35 +1,16 @@ -import { - Expression, - Node, - Statement -} from 'shift-ast'; -import { Declaration, Reference } from 'shift-scope'; +import { Node } from 'shift-ast'; +import { Declaration, Reference, Variable } from 'shift-scope'; +import pluginCommon from './refactor-plugin-common'; +import pluginUnsafe from './refactor-plugin-unsafe'; import { RefactorSession } from './refactor-session'; -import { Replacer, SimpleIdentifier, SimpleIdentifierOwner } from './types'; +import { Replacer, SelectorOrNode, SimpleIdentifier, SimpleIdentifierOwner } from './types'; -// Plugin interface lovingly taken from https://github.com/gr2m/javascript-plugin-architecture-with-typescript-definitions/blob/master/src/index.ts - -type ApiExtension = { [key: string]: any }; -type TestPlugin = (instance: RefactorSessionChainable) => ApiExtension | undefined; +// type ApiExtension = { [key: string]: any }; +// type RefactorPlugin = (instance: RefactorSessionChainable) => ApiExtension; +type RefactorPlugin = (instance: RefactorSessionChainable) => any; type Constructor = new (...args: any[]) => T; -/** - * @author https://stackoverflow.com/users/2887218/jcalz - * @see https://stackoverflow.com/a/50375286/10325032 - */ -type UnionToIntersection = (Union extends any - ? (argument: Union) => void - : never) extends (argument: infer Intersection) => void // tslint:disable-line: no-unused - ? Intersection - : never; - -type AnyFunction = (...args: any) => any; - -type ReturnTypeOf = T extends AnyFunction - ? ReturnType - : T extends AnyFunction[] - ? UnionToIntersection> - : never; +export type $QueryInput = string | Node | Node[] /** * The Chainable Refactor interface @@ -37,7 +18,7 @@ type ReturnTypeOf = T extends AnyFunction */ export class RefactorSessionChainable { session: RefactorSession; - static plugins: TestPlugin[] = []; + static plugins: RefactorPlugin[] = []; constructor(session: RefactorSession) { this.session = session; @@ -47,15 +28,34 @@ export class RefactorSessionChainable { }); } - static with & { plugins: any[] }, T extends TestPlugin | TestPlugin[]>(this: S, plugin: T) { + static with & { plugins: RefactorPlugin[] }, T extends RefactorPlugin> + (this: S, plugin: T) { const currentPlugins = this.plugins; const BaseWithPlugins = class extends this { static plugins = currentPlugins.concat(plugin); + static with = RefactorSessionChainable.with; + static create = RefactorSessionChainable.create; }; - type Extension = ReturnTypeOf; - return BaseWithPlugins as typeof BaseWithPlugins & Constructor; + return BaseWithPlugins as typeof BaseWithPlugins & Constructor>; + } + + static create(session: RefactorSession) { + const chainable = new RefactorChainableWithPlugins(session); + const prototype = Object.getPrototypeOf(chainable); + + const $query = function (selector: $QueryInput) { + const subSession = session.subSession(selector); + return RefactorChainableWithPlugins.create(subSession); + } + + const hybridObject = Object.assign($query, chainable); + Object.setPrototypeOf(hybridObject, prototype); + Object.defineProperty(hybridObject, 'length', { + get() { return session.length } + }); + return hybridObject; } get root(): Node { @@ -70,51 +70,158 @@ export class RefactorSessionChainable { return this.session.nodes; } - subSession(query: string | string[]) { - return this.session.subSession(query); - } - rename(newName: string) { - return this.session.rename(this.nodes, newName); + this.session.rename(this.nodes, newName); + return this; } + /** + * Delete nodes + * + * @example + * + * ```js + * const { refactor } = require('shift-refactor'); + * + * $script = refactor('foo();bar();'); + * + * $script('ExpressionStatement[expression.callee.name="foo"]').delete(); + * + * ``` + * + * @assert + * + * ```js + * assert.equal($script.generate(), 'bar();\n'); + * ``` + * + */ delete() { - return this.session.delete(this.nodes); + this.session.delete(this.nodes); + return this; } - replace(replacer: Replacer) { - return this.session.replace(this.nodes, replacer); + replace(replacer: Replacer): RefactorSessionChainable { + this.session.replace(this.nodes, replacer); + return this; } - replaceAsync(replacer: (node: Node) => Promise) { + /** + * Async version of .replace() that supports asynchronous replacer functions + * + * @example + * + * ```js + * const { refactor } = require('shift-refactor'); + * + * $script = refactor('var a = "hello";'); + * + * async function work() { + * await $script('LiteralStringExpression').replaceAsync( + * (node) => Promise.resolve(`"goodbye"`) + * ) + * } + * + * ``` + * @assert + * + * ```js + * // TODO this doesn't work, every async function is an instance of Promise + * work().then(_ => assert.equal($script.generate(), 'var a = "goodbye";')); + * ``` + */ + replaceAsync(replacer: (node: Node) => Promise) { return this.session.replaceAsync(this.nodes, replacer); } - replaceRecursive(replacer: Replacer) { - return this.session.replaceRecursive(this.nodes, replacer); + + /** + * Recursively replaces child nodes until no nodes have been replaced. + * + * @example + * + * ```js + * const { refactor } = require('shift-refactor'); + * const Shift = require('shift-ast'); + * + * const src = ` + * 1 + 2 + 3 + * ` + * + * $script = refactor(src); + * + * $script.replaceChildren( + * 'BinaryExpression[left.type=LiteralNumericExpression][right.type=LiteralNumericExpression]', + * (node) => new Shift.LiteralNumericExpression({value: node.left.value + node.right.value}) + * ); + * ``` + * + * @assert + * + * ```js + * assert.equal($script.generate().trim(), '6;'); + * ``` + * + */ + replaceChildren(query: SelectorOrNode, replacer: Replacer): RefactorSessionChainable { + this.session.replaceRecursive(query, replacer); + return this; } first(): Node { return this.session.first(); } - parents(): Node[] { - return this.session.findParents(this.nodes); + /** + * Retrieve parent node(s) + * + * @example + * + * ```js + * const { refactor } = require('shift-refactor'); + * + * const src = ` + * var a = 1, b = 2; + * ` + * + * $script = refactor(src); + * const declarators = $script('VariableDeclarator'); + * const declaration = declarators.parents(); + * ``` + * + * @assert + * + * ```js + * assert.equal(declaration.length, 1); + * ``` + * + */ + + parents() { + return this.$(this.session.findParents(this.nodes)); } - prepend(replacer: Replacer) { - return this.session.prepend(this.nodes, replacer); + // TODO appendInto prependInto to auto-insert into body blocks + + prepend(replacer: Replacer): RefactorSessionChainable { + this.session.prepend(this.nodes, replacer); + return this; + } + + append(replacer: Replacer): RefactorSessionChainable { + this.session.append(this.nodes, replacer); + return this; } - append(replacer: Replacer) { - return this.session.append(this.nodes, replacer); + $(queryOrNodes: SelectorOrNode) { + return RefactorSessionChainable.create(this.session.subSession(queryOrNodes)); } query(selector: string | string[]) { - return this.session.query(selector); + return this.$(this.session.query(selector)); } - forEach(iterator: (node: any) => any) { + forEach(iterator: (node: any) => any): RefactorSessionChainable { this.nodes.forEach(iterator); return this; } @@ -124,39 +231,57 @@ export class RefactorSessionChainable { return this.query(selectorOrNode); } - findMatchingExpression(sampleSrc: string): Expression[] { - return this.session.findMatchingExpression(sampleSrc); + findMatchingExpression(sampleSrc: string) { + return this.$(this.session.findMatchingExpression(sampleSrc)); } - findMatchingStatement(sampleSrc: string): Statement[] { - return this.session.findMatchingStatement(sampleSrc); + findMatchingStatement(sampleSrc: string) { + return this.$(this.session.findMatchingStatement(sampleSrc)); } findOne(selectorOrNode: string) { - return this.session.findOne(selectorOrNode); + return this.$(this.session.findOne(selectorOrNode)); } - findReferences(): Reference[] { + references(): Reference[] { return this.session.findReferences(this.first() as SimpleIdentifier | SimpleIdentifierOwner); } - findDeclarations(): Declaration[] { + declarations(): Declaration[] { return this.session.findDeclarations(this.first() as SimpleIdentifier | SimpleIdentifierOwner) } - closest(closestSelector: string): Node[] { - return this.session.closest(this.nodes, closestSelector); + closest(closestSelector: string) { + return this.$(this.session.closest(this.nodes, closestSelector)); } - lookupVariable() { - return this.session.lookupVariable(this.first() as SimpleIdentifierOwner | SimpleIdentifierOwner[] | SimpleIdentifier | SimpleIdentifier[]) + lookupVariable(): Variable { + const id = this.first() as SimpleIdentifierOwner | SimpleIdentifierOwner[] | SimpleIdentifier | SimpleIdentifier[]; + return this.session.lookupVariable(id); } - lookupVariableByName(name: string) { + lookupVariableByName(name: string): Variable[] { return this.session.lookupVariableByName(name); } - print(ast?: Node) { - return this.session.print(); + generate() { + return this.session.generate(); } -} \ No newline at end of file + + print() { + return this.session.generate(); + } + +} + +export const RefactorChainableWithPlugins = RefactorSessionChainable.with(pluginUnsafe).with(pluginCommon); + +/** + * Initialization of a RefactorSession via the chainable API + * + * @alpha + */ +export function refactor(input: string | Node, { autoCleanup = true } = {}) { + const globalSession = new RefactorSession(input, { autoCleanup }); + return RefactorSessionChainable.with(pluginUnsafe).with(pluginCommon).create(globalSession); +} diff --git a/src/refactor-session.ts b/src/refactor-session.ts index aa4b9fe..01a7322 100644 --- a/src/refactor-session.ts +++ b/src/refactor-session.ts @@ -3,14 +3,10 @@ import DEBUG from 'debug'; import deepEqual from 'fast-deep-equal'; import { BindingIdentifier, - Expression, - FunctionDeclaration, IdentifierExpression, Node, - - Statement } from 'shift-ast'; import { parseScript } from 'shift-parser'; @@ -30,9 +26,11 @@ import { isFunction, isShiftNode, isStatement, - isString + isString, + identityLogger } from './util'; import { waterfallMap } from './waterfall'; +import { RefactorSessionChainable } from './refactor-session-chainable'; /** * Parse JavaScript source with shift-parser @@ -55,12 +53,11 @@ export interface RefactorConfig { * The main Shift Refactor class * @public */ -export class RefactorSession implements ArrayLike { +export class RefactorSession { nodes: Node[]; _root?: Node; globalSession: RefactorSession; autoCleanup = true; - readonly [n: number]: Node; private dirty = false; @@ -124,23 +121,16 @@ export class RefactorSession implements ArrayLike { return this.nodes.length; } - subSession(query: string | string[]) { - const nodes = findNodes(this.nodes, query); + $(querySessionOrNodes: SelectorOrNode | RefactorSession) { + return this.subSession(querySessionOrNodes); + } + + subSession(querySessionOrNodes: SelectorOrNode | RefactorSession) { + const nodes = querySessionOrNodes instanceof RefactorSession ? querySessionOrNodes.nodes : findNodes(this.nodes, querySessionOrNodes); const subSession = new RefactorSession(nodes, { parentSession: this }); return subSession; } - /** - * Register plugin - * - * @remarks - * - * Experimental. - * - * @param Plugin - The Refactor plugin - * - * @alpha - */ use(Plugin: new (session: RefactorSession) => T) { const plugin = new Plugin(this); plugin.register(); @@ -150,15 +140,6 @@ export class RefactorSession implements ArrayLike { this.parentMap = buildParentMap(this.nodes); } - /** - * Rename Identifiers - * - * @remarks - * - * Only works on Identifier nodes. Other nodes are ignored. - * - * @param selectorOrNode - A selector or node - */ rename(selectorOrNode: SelectorOrNode, newName: string) { const lookupTable = this.getLookupTable(); @@ -174,39 +155,12 @@ export class RefactorSession implements ArrayLike { return this; } - /** - * Rename all declarations and references of a Variable lookup to newName - * - * @param lookup - * @param newName - * - * @internal - */ renameInPlace(lookup: Variable, newName: string) { if (!lookup || !newName) return; lookup.declarations.forEach(decl => ((decl.node as BindingIdentifier).name = newName)); lookup.references.forEach(ref => ((ref.node as IdentifierExpression).name = newName)); } - /** - * Delete nodes - * - * @example - * - * ```js - * const { RefactorSession, parse } = require('shift-refactor'); - * - * $script = new RefactorSession(parse('foo();bar();')); - * - * $script.delete('ExpressionStatement[expression.callee.name="foo"]'); - * - * ``` - * - * @assert - * - * assert.equal($script.print(), 'bar();\n'); - */ - delete(selectorOrNode: SelectorOrNode = this.nodes) { const nodes = findNodes(this.nodes, selectorOrNode); if (nodes.length > 0) { @@ -215,12 +169,6 @@ export class RefactorSession implements ArrayLike { return this.conditionalCleanup(); } - /** - * Replace nodes - * - * @param selectorOrNode - * @param replacer - JavaScript source, a Node, or a function that returns source or a node - */ replace(selectorOrNode: SelectorOrNode, replacer: Replacer) { const nodes = findNodes(this.nodes, selectorOrNode); @@ -268,29 +216,7 @@ export class RefactorSession implements ArrayLike { return replaced.filter((wasReplaced: any) => wasReplaced).length; } - /** - * Async version of .replace() that supports asynchronous replacer functions - * - * @example - * - * ```js - * const { RefactorSession, parse } = require('shift-refactor'); - * - * $script = new RefactorSession(parse('var a = "hello";')); - * - * async function work() { - * await $script.replaceAsync( - * 'LiteralStringExpression', - * async (node) => Promise.resolve(`"goodbye"`) - * ) - * } - * - * ``` - * @assert - * - * assert(work() instanceof Promise); - */ - async replaceAsync(selectorOrNode: SelectorOrNode, replacer: (node: Node) => Promise) { + async replaceAsync(selectorOrNode: SelectorOrNode, replacer: (node: Node) => Promise): Promise { const nodes = findNodes(this.nodes, selectorOrNode); if (!isFunction(replacer)) { @@ -323,34 +249,9 @@ export class RefactorSession implements ArrayLike { this.conditionalCleanup(); - return promiseResults.filter(result => result); - } - - /** - * Recursively replaces nodes until no nodes have been replaced. - * - * @example - * - * ```js - * const { RefactorSession, parse } = require('shift-refactor'); - * const Shift = require('shift-ast'); - * - * const src = ` - * 1 + 2 + 3 - * ` - * - * $script = new RefactorSession(parse(src)); - * - * $script.replaceRecursive( - * 'BinaryExpression[left.type=LiteralNumericExpression][right.type=LiteralNumericExpression]', - * (node) => new Shift.LiteralNumericExpression({value: node.left.value + node.right.value}) - * ); - * ``` - * - * @assert - * - * assert.equal($script.print().trim(), '6;'); - */ + return promiseResults.filter(result => result).length; + } + replaceRecursive(selectorOrNode: SelectorOrNode, replacer: Replacer) { const nodesReplaced = this.replace(selectorOrNode, replacer); this.cleanup(); @@ -395,38 +296,17 @@ export class RefactorSession implements ArrayLike { return this.conditionalCleanup(); } - /** - * Find the parent of a node - * - * @example - * - * ```js - * const { RefactorSession, parse } = require('shift-refactor'); - * const Shift = require('shift-ast'); - * - * const src = ` - * 1 + 2 + 3 - * ` - * - * $script = new RefactorSession(parse(src)); - * ``` - * - * @assert - * - * assert.equal($script.print().trim(), '6;'); - */ findParents(selectorOrNode: SelectorOrNode): Node[] { const nodes = findNodes(this.nodes, selectorOrNode); - const a = this.parentMap.get(nodes[0]); - return nodes.map(node => this.parentMap.get(node)).filter((node): node is Node => !!node); + return nodes.map(identityLogger).map(node => this.globalSession.parentMap.get(node)).map(identityLogger).filter((node): node is Node => !!node); } prepend(selectorOrNode: SelectorOrNode, replacer: Replacer) { - return this.insert(selectorOrNode, replacer, false); + return this.globalSession.insert(selectorOrNode, replacer, false); } append(selectorOrNode: SelectorOrNode, replacer: Replacer) { - return this.insert(selectorOrNode, replacer, true); + return this.globalSession.insert(selectorOrNode, replacer, true); } _queueDeletion(node: Node): void { @@ -647,11 +527,16 @@ export class RefactorSession implements ArrayLike { return Array.from(varSet) as Variable[]; } - print(ast?: Node) { + generate(ast?: Node) { if (this.isDirty()) throw new RefactorError( 'refactor .print() called with a dirty AST. This is almost always a bug. Call .cleanup() before printing.', ); return codegen(ast || this.first(), new FormattedCodeGen()); } +} + +export class GlobalSession extends RefactorSession { + type = 'GlobalSession'; + } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 487e483..f08b97d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,6 +19,7 @@ export type Replacer = Function | Node | string; // Identifiers that are easy to reason about export type SimpleIdentifier = BindingIdentifier | IdentifierExpression | AssignmentTargetIdentifier; + // Nodes containing a SimpleIdentifier that are similarly easy to reason about export type SimpleIdentifierOwner = | AssignmentExpression @@ -27,3 +28,4 @@ export type SimpleIdentifierOwner = | FunctionDeclaration | FunctionExpression | VariableDeclarator; + diff --git a/src/util.ts b/src/util.ts index f6998b1..977a2d5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -19,11 +19,11 @@ import { } from 'shift-ast'; import { SelectorOrNode, RefactorError } from './types'; import { Scope } from 'shift-scope'; -import { IdGenerator } from './id-generator'; +import { BaseIdGenerator } from './id-generator'; import traverser from 'shift-traverser'; import { mkdir } from 'fs'; import { query } from './query'; - +import { RefactorSession } from './refactor-session'; export function copy(object: any) { return JSON.parse(JSON.stringify(object)); @@ -44,6 +44,9 @@ export function isShiftNode(input: any): input is Node { export function isStatement(input: any): input is Statement { return input && input.type && input.type.match(/(Statement|Declaration)$/); } +export function forceIntoArray(input: T | T[]): T[] { + return isArray(input) ? input : [input]; +} export function isLiteral( input: any, @@ -103,7 +106,7 @@ export function extractExpression(tree: Script) { } } -export function renameScope(scope: Scope, idGenerator: IdGenerator, parentMap: WeakMap) { +export function renameScope(scope: Scope, idGenerator: BaseIdGenerator, parentMap: WeakMap) { if (scope.type.name !== 'Global' && scope.type.name !== 'Script') { scope.variableList.forEach(variable => { if (variable.declarations.length === 0) return; @@ -179,3 +182,8 @@ export function getRootIdentifier( } } } + +export function identityLogger(x: T): T { + console.log(x); + return x; +} diff --git a/test/doclint/examples.ts b/test/doclint/examples.ts index 0789160..b2f44bc 100644 --- a/test/doclint/examples.ts +++ b/test/doclint/examples.ts @@ -1,12 +1,21 @@ import * as tsdoc from '@microsoft/api-extractor-model/node_modules/@microsoft/tsdoc'; +import { TSDocEmitter, StringBuilder } from '@microsoft/api-extractor-model/node_modules/@microsoft/tsdoc'; import api from '../../temp/shift-refactor.api.json'; import vm from 'vm'; +import { fail } from 'assert'; +import { identityLogger } from '../../src/util'; -const API = (api as unknown) as IApiItemJson; +const customConfiguration: tsdoc.TSDocConfiguration = new tsdoc.TSDocConfiguration(); +customConfiguration.addTagDefinitions([ + new tsdoc.TSDocTagDefinition({ + tagName: '@assert', + syntaxKind: tsdoc.TSDocTagSyntaxKind.BlockTag + }) +]); -const parser = new tsdoc.TSDocParser(); +const parser = new tsdoc.TSDocParser(customConfiguration); interface IApiItemJson { kind: string; @@ -16,17 +25,17 @@ interface IApiItemJson { } (function main() { - const success = walk([api]); - if (!success) throw new Error('not ok: tests failed'); + const failures = walk([api]); + if (failures > 0) throw new Error(`not ok: ${failures} tests failed`); else console.log('ok: examples passed'); })(); function walk(nodes: IApiItemJson[]) { - let success = true; + let failures = 0; nodes.forEach((node: IApiItemJson) => { if (node.docComment) { - const src = extractCode(node.docComment); - const assertion = extractCode(node.docComment, '@assert'); + const src = extractFencedBlock(node.docComment); + const assertion = extractCodeOnlyBlock(node.docComment); if (src) { const localizedSrc = src.replace('shift-refactor', '../../'); const wrappedSrc = `const assert = require('assert');\n${localizedSrc};\n ${assertion}` @@ -38,19 +47,19 @@ function walk(nodes: IApiItemJson[]) { console.log(`>>>> Error in ${node.canonicalReference}`); console.log(e); console.log(`${wrappedSrc}`); - success = false; + failures++; } } } if (node.members) { - const result = walk(node.members); - if (success) success = result; + const childFailures = walk(node.members); + failures += childFailures; } }); - return success; + return failures; } -function extractCode(tsdoc: string, type = '@example') { +function extractFencedBlock(tsdoc: string, type = '@example') { const { docComment } = parser.parseString(tsdoc); const example = docComment.customBlocks.find(x => x.blockTag.tagName === type); if (example) { @@ -61,4 +70,14 @@ function extractCode(tsdoc: string, type = '@example') { } } -// function runExample() +function extractCodeOnlyBlock(tsdoc: string, type = '@assert') { + const { docComment } = parser.parseString(tsdoc); + const block = docComment.customBlocks.find(x => x.blockTag.tagName === type) as tsdoc.DocBlock; + if (block) { + const fencedCode = block.content.nodes.find( + (contentNode: tsdoc.DocNode) => contentNode.kind === 'FencedCode', + ) as tsdoc.DocFencedCode; + return fencedCode.code; + } +} + diff --git a/test/id-generator.test.ts b/test/id-generator.test.ts index 46144b8..81462f7 100644 --- a/test/id-generator.test.ts +++ b/test/id-generator.test.ts @@ -1,28 +1,9 @@ -import {BasicIdGenerator} from '../src/id-generator'; +import { MemorableIdGenerator } from '../src/id-generator'; -const chai = require('chai'); +const { expect } = require('chai'); -describe('BasicIdGenerator', function() { - it('generate sequential identifiers', () => { - const gen = new BasicIdGenerator('abAB'); - chai.expect(gen.next().value).to.equal('a'); - chai.expect(gen.next().value).to.equal('b'); - chai.expect(gen.next().value).to.equal('A'); - chai.expect(gen.next().value).to.equal('B'); - chai.expect(gen.next().value).to.equal('aa'); - chai.expect(gen.next().value).to.equal('ab'); - chai.expect(gen.next().value).to.equal('aA'); - chai.expect(gen.next().value).to.equal('aB'); - chai.expect(gen.next().value).to.equal('ba'); - }); - it('should skip keywords', () => { - const gen = new BasicIdGenerator('doD'); - chai.expect(gen.next().value).to.equal('d'); - chai.expect(gen.next().value).to.equal('o'); - chai.expect(gen.next().value).to.equal('D'); - chai.expect(gen.next().value).to.equal('dd'); - // skips "do" - chai.expect(gen.next().value).to.equal('dD'); - chai.expect(gen.next().value).to.equal('od'); +xdescribe('MemorableIdGenerator', function () { + it('should be tested', () => { + // TODO: needs tests }); }); diff --git a/test/plugin-unsafe/pure-functions.test.ts b/test/plugin-unsafe/pure-functions.test.ts index 1cdaa9b..eb77214 100644 --- a/test/plugin-unsafe/pure-functions.test.ts +++ b/test/plugin-unsafe/pure-functions.test.ts @@ -1,91 +1,91 @@ -import { expect } from 'chai'; -import { parseScript as parse } from 'shift-parser'; -import { FunctionDeclaration, CallExpression, ExpressionStatement } from 'shift-ast'; -import { refactor } from '../../src'; -import { - PureFunctionAssessment, - ImpureFunctionQualities, - PureFunctionVerdict, - PureFunctionAssessmentOptions, -} from '../../src/pure-functions'; +// import { expect } from 'chai'; +// import { parseScript as parse } from 'shift-parser'; +// import { FunctionDeclaration, CallExpression, ExpressionStatement } from 'shift-ast'; +// import { refactor } from '../../src'; +// import { +// PureFunctionAssessment, +// ImpureFunctionQualities, +// PureFunctionVerdict, +// PureFunctionAssessmentOptions, +// } from '../../src/pure-functions'; -function assess(src: string, options?: PureFunctionAssessmentOptions) { - const ast = parse(src); - const fn = ast.statements[0]; - return new PureFunctionAssessment(fn as FunctionDeclaration, options); -} +// function assess(src: string, options?: PureFunctionAssessmentOptions) { +// const ast = parse(src); +// const fn = ast.statements[0]; +// return new PureFunctionAssessment(fn as FunctionDeclaration, options); +// } -describe('findPureFunctionCandidates', () => { - it('not consider functions that access outside scope', () => { - let ast = parse(`var outer = 2; function impure(a) {return a + outer};`); - const $script = refactor(ast); - const candidates = $script.findPureFunctionCandidates(); - expect(candidates.size).to.equal(0); - }); +// describe('findPureFunctionCandidates', () => { +// it('not consider functions that access outside scope', () => { +// let ast = parse(`var outer = 2; function impure(a) {return a + outer};`); +// const $script = refactor(ast); +// const candidates = $script.findPureFunctionCandidates(); +// expect(candidates.size).to.equal(0); +// }); - it('should pass options to assessments', () => { - let ast = parse(`function pureEnough(a) {return String.fromCharCode(a)};`); - const $script = refactor(ast); - const candidates = $script.findPureFunctionCandidates({ - fnAllowList: ['String.fromCharCode()'], - }); - expect(candidates.size).to.equal(1); - }); +// it('should pass options to assessments', () => { +// let ast = parse(`function pureEnough(a) {return String.fromCharCode(a)};`); +// const $script = refactor(ast); +// const candidates = $script.findPureFunctionCandidates({ +// fnAllowList: ['String.fromCharCode()'], +// }); +// expect(candidates.size).to.equal(1); +// }); - describe('PureFunctionAssessment', () => { - it('through access', () => { - const assessment = assess(`function impure(a) {return a + outer};`); - expect(assessment.qualities.has(ImpureFunctionQualities.ThroughAccess)).to.be.true; - expect(assessment.verdict).to.equal(PureFunctionVerdict.ProbablyNot); - }); +// describe('PureFunctionAssessment', () => { +// it('through access', () => { +// const assessment = assess(`function impure(a) {return a + outer};`); +// expect(assessment.qualities.has(ImpureFunctionQualities.ThroughAccess)).to.be.true; +// expect(assessment.verdict).to.equal(PureFunctionVerdict.ProbablyNot); +// }); - it('basic binary expression', () => { - const assessment = assess(`function add(a,b) {return a+b};`); - expect(assessment.qualities.has(ImpureFunctionQualities.ThroughAccess)).to.be.false; - expect(assessment.verdict).to.equal(PureFunctionVerdict.Probably); - }); +// it('basic binary expression', () => { +// const assessment = assess(`function add(a,b) {return a+b};`); +// expect(assessment.qualities.has(ImpureFunctionQualities.ThroughAccess)).to.be.false; +// expect(assessment.verdict).to.equal(PureFunctionVerdict.Probably); +// }); - it('parameter member mutation', () => { - let assessment = assess(`function test(a) { a.foo = b; }`); - expect(assessment.qualities.has(ImpureFunctionQualities.ParameterMemberMutation)).to.be.true; - expect(assessment.verdict).to.equal(PureFunctionVerdict.ProbablyNot); - assessment = assess(`function test([a]) { a.foo = b; }`); - expect(assessment.qualities.has(ImpureFunctionQualities.ParameterMemberMutation)).to.be.true; - expect(assessment.verdict).to.equal(PureFunctionVerdict.ProbablyNot); - }); +// it('parameter member mutation', () => { +// let assessment = assess(`function test(a) { a.foo = b; }`); +// expect(assessment.qualities.has(ImpureFunctionQualities.ParameterMemberMutation)).to.be.true; +// expect(assessment.verdict).to.equal(PureFunctionVerdict.ProbablyNot); +// assessment = assess(`function test([a]) { a.foo = b; }`); +// expect(assessment.qualities.has(ImpureFunctionQualities.ParameterMemberMutation)).to.be.true; +// expect(assessment.verdict).to.equal(PureFunctionVerdict.ProbablyNot); +// }); - it('argument member mutation', () => { - const assessment = assess(`function test(a) { arguments[0].foo = b; }`); - expect(assessment.qualities.has(ImpureFunctionQualities.ArgumentsMemberMutation)).to.be.true; - expect(assessment.verdict).to.equal(PureFunctionVerdict.ProbablyNot); - }); +// it('argument member mutation', () => { +// const assessment = assess(`function test(a) { arguments[0].foo = b; }`); +// expect(assessment.qualities.has(ImpureFunctionQualities.ArgumentsMemberMutation)).to.be.true; +// expect(assessment.verdict).to.equal(PureFunctionVerdict.ProbablyNot); +// }); - it('calls pure functions', () => { - const assessment = assess(`function test(a) { function inner() {return 2} return a + inner(); }`); - expect(assessment.verdict).to.equal(PureFunctionVerdict.Probably); - }); +// it('calls pure functions', () => { +// const assessment = assess(`function test(a) { function inner() {return 2} return a + inner(); }`); +// expect(assessment.verdict).to.equal(PureFunctionVerdict.Probably); +// }); - it('calls impure functions', () => { - const assessment = assess(`function test(a) { function inner() {return k} return a + inner(); }`); - expect(assessment.qualities.has(ImpureFunctionQualities.CallsImpureFunctions)).to.be.true; - expect(assessment.verdict).to.equal(PureFunctionVerdict.ProbablyNot); - }); +// it('calls impure functions', () => { +// const assessment = assess(`function test(a) { function inner() {return k} return a + inner(); }`); +// expect(assessment.qualities.has(ImpureFunctionQualities.CallsImpureFunctions)).to.be.true; +// expect(assessment.verdict).to.equal(PureFunctionVerdict.ProbablyNot); +// }); - it('calls allowlisted functions', () => { - const options = { - fnAllowList: ['String.fromCharCode()'], - }; - const assessment = assess(`function test(a) { return String.fromCharCode(a); }`, options); - expect(assessment.verdict).to.equal(PureFunctionVerdict.Probably); - }); - }); +// it('calls allowlisted functions', () => { +// const options = { +// fnAllowList: ['String.fromCharCode()'], +// }; +// const assessment = assess(`function test(a) { return String.fromCharCode(a); }`, options); +// expect(assessment.verdict).to.equal(PureFunctionVerdict.Probably); +// }); +// }); - it('should find functions with no calls and no through access and no writes to parameters', () => { - let ast = parse(`function add(a,b) {return a+b};function other(a,b) {return a+window.somewhereElse};`); - const $script = refactor(ast); - const functions = $script.findPureFunctionCandidates(); - expect(functions.size).to.equal(1); - const declarations = Array.from(functions.keys()); - expect(declarations[0]).to.deep.equal(ast.statements[0]); - }); -}); +// it('should find functions with no calls and no through access and no writes to parameters', () => { +// let ast = parse(`function add(a,b) {return a+b};function other(a,b) {return a+window.somewhereElse};`); +// const $script = refactor(ast); +// const functions = $script.findPureFunctionCandidates(); +// expect(functions.size).to.equal(1); +// const declarations = Array.from(functions.keys()); +// expect(declarations[0]).to.deep.equal(ast.statements[0]); +// }); +// }); diff --git a/test/refactor-chainable-api.test.ts b/test/refactor-chainable-api.test.ts index dc3351f..4a4441f 100644 --- a/test/refactor-chainable-api.test.ts +++ b/test/refactor-chainable-api.test.ts @@ -1,13 +1,13 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { parseScript as parse } from 'shift-parser'; -import { refactor } from '../src/index'; +import { refactor } from '../src/refactor-session-chainable'; import { LiteralNumericExpression } from 'shift-ast'; describe('chainable interface', () => { it('should be able to take a single source as input', () => { const src = `function foo(){}\nfoo();`; - const printedSource = refactor(src).print(); + const printedSource = refactor(src).generate(); expect(parse(printedSource)).to.deep.equal(parse(src)); }); it('every return value should be a query function scoped to the child node', () => { @@ -18,10 +18,18 @@ describe('chainable interface', () => { const $child = $script('CallExpression'); expect($child.length).to.equal(1); expect($child.first().type).to.equal('CallExpression'); + //@ts-ignore const $idExpr = $child('IdentifierExpression'); expect($idExpr.length).to.equal(1); expect($idExpr.first().type).to.equal('IdentifierExpression'); }); + it('should support chaining across methods that return nodes', () => { + const src = `b(1);`; + const $script = refactor(src); + $script('CallExpression').closest(':statement').prepend(`a()`); + + expect($script.root).to.deep.equal(parse(`a();b();`)); + }) it('should have .forEach to iterate over nodes', () => { const src = `var a = [1,2,3,4]`; const $script = refactor(src); @@ -30,6 +38,12 @@ describe('chainable interface', () => { }); expect($script.root).to.deep.equal(parse(`var a = [2,4,6,8]`)); }) + // it('should have .root() to reference global Session', () => { + // const src = `var a = [1,2,3,4]`; + // const $script = refactor(src); + // const root = $script('LiteralNumericExpression').root(); + // expect($script.generate()).to.deep.equal(root.generate()); + // }) describe('methods w/o arguments', () => { it('.delete() should delete self', () => { const src = `idExp;function foo(){}\nfoo();`; diff --git a/test/refactor-session.test.ts b/test/refactor-session.test.ts index 54ff75f..09fa0ac 100644 --- a/test/refactor-session.test.ts +++ b/test/refactor-session.test.ts @@ -124,7 +124,7 @@ describe('RefactorSession', () => { }); describe('subSession', () => { - it('should scope a refactor session to child nodes', async () => { + it('should scope a refactor session to child nodes via a query', async () => { let r = new RefactorSession(`b;function foo(a){return d}`); let rootIdExprs = r.query('IdentifierExpression'); expect(rootIdExprs.length).to.equal(2); @@ -133,6 +133,16 @@ describe('RefactorSession', () => { let idExpr = callExpression.query('IdentifierExpression'); expect(idExpr.length).to.equal(1); }); + it('should scope a refactor session to nodes passed as arguments', async () => { + let r = new RefactorSession(`b;function foo(a){return d}`); + let rootIdExprs = r.query('IdentifierExpression'); + expect(rootIdExprs.length).to.equal(2); + let callExpressions = r.subSession('FunctionDeclaration'); + expect(callExpressions.nodes.length).to.equal(1); + const subSession = r.subSession(callExpressions); + let idExpr = subSession.query('IdentifierExpression'); + expect(idExpr.length).to.equal(1); + }); }); describe('replaceAsync', () => { it('should replace nodes with Node instances', async () => { @@ -287,13 +297,13 @@ describe('RefactorSession', () => { it('should print a structurally equivalent program', () => { let ast = parse(`var a = 2; function foo(){var a = 4}`); const refactor = new RefactorSession(ast); - const newSource = refactor.print(); + const newSource = refactor.generate(); expect(ast).to.deep.equal(parse(newSource)); }); it('should take in and print any ast', () => { let ast = parse(`var a = 2; function foo(){var a = 4}`); const refactor = new RefactorSession(ast); - const newSource = refactor.print(new LiteralStringExpression({ value: 'hi' })); + const newSource = refactor.generate(new LiteralStringExpression({ value: 'hi' })); expect(newSource).to.equal('"hi"'); }); }) diff --git a/test/regression.test.ts b/test/regression.test.ts index 1a5e77d..bcd659e 100644 --- a/test/regression.test.ts +++ b/test/regression.test.ts @@ -1,4 +1,4 @@ -import { refactor } from '../src/'; +import { refactor } from '../src/refactor-session-chainable'; import { parseScript as parse } from 'shift-parser'; import Shift from 'shift-ast'; @@ -23,7 +23,7 @@ describe('Regression', function () { let ast = parse(`var a = 2, b = 3;`); const $script = refactor(ast); function danger() { - $script('VariableDeclarator').replaceRecursive((node: any) => node); + $script.replaceChildren('VariableDeclarator', (node: any) => node); } chai.expect(danger).to.not.throw(); });