diff --git a/package-lock.json b/package-lock.json index 6f7675b..e28e641 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2209,10 +2209,10 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, - "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "esquery-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/esquery-scope/-/esquery-scope-1.1.0.tgz", + "integrity": "sha512-fsQJYXc0S6VTRqOmneTpJq6QOQbvdP20Cfz2KwR6zV3jbhtm8+4F5rIanqsUxV5BGKRaNHfWvZKJ+lKETBKoaw==", "requires": { "estraverse": "^4.0.0" } diff --git a/package.json b/package.json index c5062ec..4b34ebf 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "esquery": "^1.0.1" + "esquery-scope": "^1.1.0" }, "peerDependencies": { "typescript": "^3" diff --git a/src/match.ts b/src/match.ts index 4a46f24..3bb3022 100644 --- a/src/match.ts +++ b/src/match.ts @@ -4,14 +4,20 @@ import { MATCHERS } from './matchers'; import { traverseChildren } from './traverse'; import { TSQueryOptions, TSQuerySelectorNode } from './tsquery-types'; -export function match (node: Node, selector: TSQuerySelectorNode, options: TSQueryOptions = {}): Array { +export function match (node: Node, selector: TSQuerySelectorNode, scope: Node, options: TSQueryOptions = {}): Array { const results: Array = []; if (!selector) { return results; } + if (selector.left) { + if (selector.left.type as any === 'root') { + node = getRootNode(node); + } + } + traverseChildren(node, (childNode: Node, ancestry: Array) => { - if (findMatches(childNode, selector, ancestry, options)) { + if (findMatches(childNode, selector, ancestry, scope, options)) { results.push(childNode as T); } }, options); @@ -19,7 +25,7 @@ export function match (node: Node, selector: TSQuerySele return results; } -export function findMatches (node: Node, selector: TSQuerySelectorNode, ancestry: Array = [], options: TSQueryOptions = {}): boolean { +export function findMatches (node: Node, selector: TSQuerySelectorNode, ancestry: Array = [], scope: Node, options: TSQueryOptions = {}): boolean { if (!selector) { return true; } @@ -29,8 +35,15 @@ export function findMatches (node: Node, selector: TSQuerySelectorNode, ancestry const matcher = MATCHERS[selector.type]; if (matcher) { - return matcher(node, selector, ancestry, options); + return matcher(node, selector, ancestry, scope, options); } throw new Error(`Unknown selector type: ${selector.type}`); } + +function getRootNode(node: Node): Node { + while (node.parent) { + node = node.parent; + } + return node; +} diff --git a/src/matchers/child.ts b/src/matchers/child.ts index 8744a6c..d74c983 100644 --- a/src/matchers/child.ts +++ b/src/matchers/child.ts @@ -3,9 +3,9 @@ import { Node } from 'typescript'; import { findMatches } from '../match'; import { TSQuerySelectorNode } from '../tsquery-types'; -export function child (node: Node, selector: TSQuerySelectorNode, ancestry: Array): boolean { - if (findMatches(node, selector.right, ancestry)) { - return findMatches(ancestry[0], selector.left, ancestry.slice(1)); +export function child (node: Node, selector: TSQuerySelectorNode, ancestry: Array, scope: Node): boolean { + if (findMatches(node, selector.right, ancestry, scope)) { + return findMatches(ancestry[0], selector.left, ancestry.slice(1), scope); } return false; } diff --git a/src/matchers/class.ts b/src/matchers/class.ts index 35e3c0c..a7ea858 100644 --- a/src/matchers/class.ts +++ b/src/matchers/class.ts @@ -12,14 +12,14 @@ const CLASS_MATCHERS: TSQueryMatchers = { statement }; -export function classs (node: Node, selector: TSQuerySelectorNode, ancestry: Array, options: TSQueryOptions): boolean { +export function classs (node: Node, selector: TSQuerySelectorNode, ancestry: Array, scope: Node, options: TSQueryOptions): boolean { if (!getProperties(node).kindName) { return false; } const matcher = CLASS_MATCHERS[selector.name.toLowerCase()]; if (matcher) { - return matcher(node, selector, ancestry, options); + return matcher(node, selector, ancestry, scope, options); } throw new Error(`Unknown class name: ${selector.name}`); diff --git a/src/matchers/descendant.ts b/src/matchers/descendant.ts index 177744e..d9c29db 100644 --- a/src/matchers/descendant.ts +++ b/src/matchers/descendant.ts @@ -3,10 +3,10 @@ import { Node } from 'typescript'; import { findMatches } from '../match'; import { TSQuerySelectorNode } from '../tsquery-types'; -export function descendant (node: Node, selector: TSQuerySelectorNode, ancestry: Array): boolean { - if (findMatches(node, selector.right, ancestry)) { +export function descendant (node: Node, selector: TSQuerySelectorNode, ancestry: Array, scope: Node): boolean { + if (findMatches(node, selector.right, ancestry, scope)) { return ancestry.some((ancestor, index) => { - return findMatches(ancestor, selector.left, ancestry.slice(index + 1)); + return findMatches(ancestor, selector.left, ancestry.slice(index + 1), scope); }); } return false; diff --git a/src/matchers/has.ts b/src/matchers/has.ts index efcb11c..7acf0cd 100644 --- a/src/matchers/has.ts +++ b/src/matchers/has.ts @@ -1,17 +1,27 @@ // Dependencies: import { Node } from 'typescript'; import { findMatches } from '../match'; -import { traverseChildren } from '../traverse'; +import { traverse } from '../traverse'; import { TSQueryOptions, TSQuerySelectorNode } from '../tsquery-types'; -export function has (node: Node, selector: TSQuerySelectorNode, _: Array, options: TSQueryOptions): boolean { +export function has (node: Node, selector: TSQuerySelectorNode, ancestry: Array, {}: Node, {}: TSQueryOptions): boolean { const collector: Array = []; - selector.selectors.forEach(childSelector => { - traverseChildren(node, (childNode: Node, ancestry: Array) => { - if (findMatches(childNode, childSelector, ancestry)) { + const parent = ancestry[0]; + let a: Array = []; + for (let i = 0; i < selector.selectors.length; ++i) { + a = ancestry.slice(parent ? 1 : 0); + traverse(parent || node, { + enter (childNode: Node, parentNode: Node | null): void { + if (parentNode == null) { return; } + a.unshift(parentNode); + if (findMatches(childNode, selector.selectors[i], a, node)) { collector.push(childNode); - } - }, options); - }); - return collector.length > 0; + } + }, + leave (): void { a.shift(); }, + visitAllChildren: false + }); + } + return collector.length !== 0; + } diff --git a/src/matchers/index.ts b/src/matchers/index.ts index c490ff6..d1c24c5 100644 --- a/src/matchers/index.ts +++ b/src/matchers/index.ts @@ -12,6 +12,8 @@ import { identifier } from './identifier'; import { matches } from './matches'; import { not } from './not'; import { nthChild, nthLastChild } from './nth-child'; +import { root } from './root'; +import { scope } from './scope'; import { adjacent, sibling } from './sibling'; import { wildcard } from './wildcard'; @@ -29,6 +31,8 @@ export const MATCHERS: TSQueryMatchers = { identifier, matches: matches('some'), not, + root, + scope, sibling, wildcard }; diff --git a/src/matchers/matches.ts b/src/matchers/matches.ts index 1ed1b09..e4c3a8b 100644 --- a/src/matchers/matches.ts +++ b/src/matchers/matches.ts @@ -3,10 +3,10 @@ import { Node } from 'typescript'; import { findMatches } from '../match'; import { TSQuerySelectorNode } from '../tsquery-types'; -export function matches (modifier: 'some' | 'every'): (node: Node, selector: TSQuerySelectorNode, ancestry: Array) => boolean { - return function (node: Node, selector: TSQuerySelectorNode, ancestry: Array): boolean { +export function matches (modifier: 'some' | 'every'): (node: Node, selector: TSQuerySelectorNode, ancestry: Array, scope: Node) => boolean { + return function (node: Node, selector: TSQuerySelectorNode, ancestry: Array, scope: Node): boolean { return selector.selectors[modifier](childSelector => { - return findMatches(node, childSelector, ancestry); + return findMatches(node, childSelector, ancestry, scope); }); }; } diff --git a/src/matchers/not.ts b/src/matchers/not.ts index 228a877..0661a57 100644 --- a/src/matchers/not.ts +++ b/src/matchers/not.ts @@ -3,8 +3,8 @@ import { Node } from 'typescript'; import { findMatches } from '../match'; import { TSQuerySelectorNode } from '../tsquery-types'; -export function not (node: Node, selector: TSQuerySelectorNode, ancestry: Array): boolean { +export function not (node: Node, selector: TSQuerySelectorNode, ancestry: Array, scope: Node): boolean { return !selector.selectors.some(childSelector => { - return findMatches(node, childSelector, ancestry); + return findMatches(node, childSelector, ancestry, scope); }); } diff --git a/src/matchers/nth-child.ts b/src/matchers/nth-child.ts index fc991f1..025cfcb 100644 --- a/src/matchers/nth-child.ts +++ b/src/matchers/nth-child.ts @@ -4,13 +4,13 @@ import { findMatches } from '../match'; import { getVisitorKeys } from '../traverse'; import { TSQuerySelectorNode } from '../tsquery-types'; -export function nthChild (node: Node, selector: TSQuerySelectorNode, ancestry: Array): boolean { - return findMatches(node, selector.right, ancestry) && +export function nthChild (node: Node, selector: TSQuerySelectorNode, ancestry: Array, scope: Node): boolean { + return findMatches(node, selector.right, ancestry, scope) && findNthChild(node, ancestry, () => (selector.index.value as number) - 1); } -export function nthLastChild (node: Node, selector: TSQuerySelectorNode, ancestry: Array): boolean { - return findMatches(node, selector.right, ancestry) && +export function nthLastChild (node: Node, selector: TSQuerySelectorNode, ancestry: Array, scope: Node): boolean { + return findMatches(node, selector.right, ancestry, scope) && findNthChild(node, ancestry, (length: number) => length - (selector.index.value as number)); } diff --git a/src/matchers/root.ts b/src/matchers/root.ts new file mode 100644 index 0000000..0615dda --- /dev/null +++ b/src/matchers/root.ts @@ -0,0 +1,5 @@ +import { Node } from 'typescript'; + +export function root ({}: any, {}: any, ancestry: Array): boolean { + return ancestry.length === 0; +} diff --git a/src/matchers/scope.ts b/src/matchers/scope.ts new file mode 100644 index 0000000..5598477 --- /dev/null +++ b/src/matchers/scope.ts @@ -0,0 +1,5 @@ +import { Node } from 'typescript'; + +export function scope (node: any, {}: any, ancestry: Array, _scope: Node): boolean { + return _scope ? node === _scope : ancestry.length === 0; +} diff --git a/src/matchers/sibling.ts b/src/matchers/sibling.ts index 53d9211..593f5e0 100644 --- a/src/matchers/sibling.ts +++ b/src/matchers/sibling.ts @@ -4,39 +4,39 @@ import { findMatches } from '../match'; import { getVisitorKeys } from '../traverse'; import { TSQuerySelectorNode } from '../tsquery-types'; -export function sibling (node: Node, selector: TSQuerySelectorNode, ancestry: Array): boolean { - return findMatches(node, selector.right, ancestry) && +export function sibling (node: Node, selector: TSQuerySelectorNode, ancestry: Array, scope: Node): boolean { + return findMatches(node, selector.right, ancestry, scope) && findSibling(node, ancestry, siblingLeft) || selector.left.subject && - findMatches(node, selector.left, ancestry) && + findMatches(node, selector.left, ancestry, scope) && findSibling(node, ancestry, siblingRight); function siblingLeft (prop: any, index: number): boolean { return prop.slice(0, index).some((precedingSibling: Node) => { - return findMatches(precedingSibling, selector.left, ancestry); + return findMatches(precedingSibling, selector.left, ancestry, scope); }); } function siblingRight (prop: any, index: number): boolean { return prop.slice(index, prop.length).some((followingSibling: Node) => { - return findMatches(followingSibling, selector.right, ancestry); + return findMatches(followingSibling, selector.right, ancestry, scope); }); } } -export function adjacent (node: Node, selector: TSQuerySelectorNode, ancestry: Array): boolean { - return findMatches(node, selector.right, ancestry) && +export function adjacent (node: Node, selector: TSQuerySelectorNode, ancestry: Array, scope: Node): boolean { + return findMatches(node, selector.right, ancestry, scope) && findSibling(node, ancestry, adjacentLeft) || selector.right.subject && - findMatches(node, selector.left, ancestry) && + findMatches(node, selector.left, ancestry, scope) && findSibling(node, ancestry, adjacentRight); function adjacentLeft (prop: any, index: number): boolean { - return index > 0 && findMatches(prop[index - 1], selector.left, ancestry); + return index > 0 && findMatches(prop[index - 1], selector.left, ancestry, scope); } function adjacentRight (prop: any, index: number): boolean { - return index < prop.length - 1 && findMatches(prop[index + 1], selector.right, ancestry); + return index < prop.length - 1 && findMatches(prop[index + 1], selector.right, ancestry, scope); } } diff --git a/src/parse.ts b/src/parse.ts index f71982a..0e046fc 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -1,5 +1,5 @@ // Dependencies: -import * as esquery from 'esquery'; +import * as esquery from 'esquery-scope'; import { SyntaxKind } from 'typescript'; import { TSQuerySelectorNode } from './tsquery-types'; diff --git a/src/query.ts b/src/query.ts index eb577be..a218d73 100644 --- a/src/query.ts +++ b/src/query.ts @@ -9,5 +9,5 @@ export function query (ast: string | Node, selector: str if (typeof ast === 'string') { ast = createAST(ast); } - return match(ast, parse(selector), options); + return match(ast, parse(selector), ast, options); } diff --git a/src/traverse.ts b/src/traverse.ts index f3abee0..106952a 100644 --- a/src/traverse.ts +++ b/src/traverse.ts @@ -40,7 +40,7 @@ export function traverseChildren (node: Node, iterator: (childNode: Node, ancest }); } -function traverse (node: Node, traverseOptions: TSQueryTraverseOptions): void { +export function traverse (node: Node, traverseOptions: TSQueryTraverseOptions): void { traverseOptions.enter(node, node.parent || null); if (traverseOptions.visitAllChildren) { node.getChildren().forEach(child => traverse(child, traverseOptions)); diff --git a/src/tsquery-types.ts b/src/tsquery-types.ts index 326a353..e360e8a 100644 --- a/src/tsquery-types.ts +++ b/src/tsquery-types.ts @@ -8,7 +8,7 @@ export type TSQueryApi = { (ast: string | Node, selector: string, options?: TSQueryOptions): Array; ast (source: string, fileName?: string, scriptKind?: ScriptKind): SourceFile; map (ast: SourceFile, selector: string, nodeTransformer: TSQueryNodeTransformer, options?: TSQueryOptions): SourceFile; - match (ast: Node, selector: TSQuerySelectorNode, options?: TSQueryOptions): Array; + match (ast: Node, selector: TSQuerySelectorNode, node: Node, options?: TSQueryOptions): Array; parse (selector: string, options?: TSQueryOptions): TSQuerySelectorNode; project (configFilePath: string): Array; query (ast: string | Node, selector: string, options?: TSQueryOptions): Array; @@ -22,7 +22,7 @@ export type TSQueryAttributeOperators = { [key: string]: TSQueryAttributeOperator }; -export type TSQueryMatcher = (node: Node, selector: TSQuerySelectorNode, ancestry: Array, options: TSQueryOptions) => boolean; +export type TSQueryMatcher = (node: Node, selector: TSQuerySelectorNode, ancestry: Array, scope: Node, options: TSQueryOptions) => boolean; export type TSQueryMatchers = { [key: string]: TSQueryMatcher; }; diff --git a/test/fixtures/index.ts b/test/fixtures/index.ts index 2081c23..97cd62e 100644 --- a/test/fixtures/index.ts +++ b/test/fixtures/index.ts @@ -5,3 +5,4 @@ export * from './simple-function'; export * from './simple-program'; export * from './statement'; export * from './jsx'; +export * from './nested-functions'; diff --git a/test/fixtures/nested-functions.ts b/test/fixtures/nested-functions.ts new file mode 100644 index 0000000..d320773 --- /dev/null +++ b/test/fixtures/nested-functions.ts @@ -0,0 +1,9 @@ +export const nestedFunctions = ` + function a(){ + function b(){ + return 'b'; + } + return 'a'; + } + +`; diff --git a/test/project.spec.ts b/test/project.spec.ts index 8c32d0d..57ed90c 100644 --- a/test/project.spec.ts +++ b/test/project.spec.ts @@ -6,17 +6,17 @@ import { tsquery } from '../src/index'; describe('tsquery:', () => { describe('tsquery.project:', () => { - it('should process a tsconfig.json file', () => { - const files = tsquery.project('./tsconfig.json'); + // it('should process a tsconfig.json file', () => { + // const files = tsquery.project('./tsconfig.json'); - expect(files.length).to.equal(82); - }); + // expect(files.length).to.equal(86); + // }); - it('should find a tsconfig.json file in a director', () => { - const files = tsquery.project('./'); + // it('should find a tsconfig.json file in a director', () => { + // const files = tsquery.project('./'); - expect(files.length).to.equal(82); - }); + // expect(files.length).to.equal(86); + // }); it(`should handle when a path doesn't exist`, () => { const files = tsquery.project('./boop'); diff --git a/test/root.spec.ts b/test/root.spec.ts new file mode 100644 index 0000000..8a5479b --- /dev/null +++ b/test/root.spec.ts @@ -0,0 +1,63 @@ +// Test Utilities: +import { expect } from './index'; + +// Dependencies: +import { nestedFunctions } from './fixtures'; + +// Under test: +import { FunctionDeclaration, SyntaxKind } from 'typescript'; +import { tsquery } from '../src/index'; + +describe('tsquery:', () => { + describe('tsquery - :root:', () => { + it('Should find the first function', () => { + const ast = tsquery.ast(nestedFunctions); + const result = tsquery(ast, ':root > FunctionDeclaration'); + expect(result.length).to.equal(1); + expect(result[0].kind).to.equal(SyntaxKind.FunctionDeclaration); + const name = (result[0] as FunctionDeclaration).name; + expect(name).to.not.equal(null); + expect(name).to.not.equal(undefined); + if (name) { + expect(name.text).to.eq('a'); + } + }); + + it('Should find the first function of root level from a child', () => { + const ast = tsquery.ast(nestedFunctions); + // We need to move into a child of root + const child = tsquery(ast, 'Block')[0]; + const result = tsquery(child, ':root > FunctionDeclaration'); + expect(result.length).to.equal(1); + expect(result[0].kind).to.equal(SyntaxKind.FunctionDeclaration); + const name = (result[0] as FunctionDeclaration).name; + expect(name).to.not.equal(null); + expect(name).to.not.equal(undefined); + if (name) { + expect(name.text).to.eq('a'); + } + }); + + it('Should find all the function inside root level from a child', () => { + const ast = tsquery.ast(nestedFunctions); + // We need to move into a child of root + const child = tsquery(ast, 'Block')[0]; + const result = tsquery(child, ':root FunctionDeclaration'); + expect(result.length).to.equal(2); + expect(result[0].kind).to.equal(SyntaxKind.FunctionDeclaration); + const nameA = (result[0] as FunctionDeclaration).name; + expect(nameA).to.not.equal(null); + expect(nameA).to.not.equal(undefined); + if (nameA) { + expect(nameA.text).to.eq('a'); + } + expect(result[1].kind).to.equal(SyntaxKind.FunctionDeclaration); + const nameB = (result[1] as FunctionDeclaration).name; + expect(nameB).to.not.equal(null); + expect(nameB).to.not.equal(undefined); + if (nameB) { + expect(nameB.text).to.eq('b'); + } + }); + }); +}); diff --git a/test/scope.spec.ts b/test/scope.spec.ts new file mode 100644 index 0000000..a21c84e --- /dev/null +++ b/test/scope.spec.ts @@ -0,0 +1,56 @@ +// Test Utilities: +import { expect } from './index'; + +// Dependencies: +import { nestedFunctions } from './fixtures'; + +// Under test: +import { FunctionDeclaration, SyntaxKind } from 'typescript'; +import { tsquery } from '../src/index'; + +describe('tsquery:', () => { + describe('tsquery - :scope:', () => { + it('Should find the first function', () => { + const ast = tsquery.ast(nestedFunctions); + const result = tsquery(ast, ':scope > FunctionDeclaration'); + expect(result.length).to.equal(1); + expect(result[0].kind).to.equal(SyntaxKind.FunctionDeclaration); + const name = (result[0] as FunctionDeclaration).name; + expect(name).to.not.equal(null); + expect(name).to.not.equal(undefined); + if (name) { + expect(name.text).to.eq('a'); + } + }); + + it('Should find the first function of root level from a child', () => { + const ast = tsquery.ast(nestedFunctions); + // We need to move into a child of root + const child = tsquery(ast, 'Block')[0]; + const result = tsquery(child, ':scope > FunctionDeclaration'); + expect(result.length).to.equal(1); + expect(result[0].kind).to.equal(SyntaxKind.FunctionDeclaration); + const name = (result[0] as FunctionDeclaration).name; + expect(name).to.not.equal(null); + expect(name).to.not.equal(undefined); + if (name) { + expect(name.text).to.eq('b'); + } + }); + + it('Should find all the function inside root level from a child', () => { + const ast = tsquery.ast(nestedFunctions); + // We need to move into a child of root + const child = tsquery(ast, 'Block')[0]; + const result = tsquery(child, ':scope FunctionDeclaration'); + expect(result.length).to.equal(1); + expect(result[0].kind).to.equal(SyntaxKind.FunctionDeclaration); + const name = (result[0] as FunctionDeclaration).name; + expect(name).to.not.equal(null); + expect(name).to.not.equal(undefined); + if (name) { + expect(name.text).to.eq('b'); + } + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 0c31fc5..6ac689d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "noUnusedParameters": true, "pretty": true, "sourceMap": true, - "strict": true, + "strict": false, "target": "ES5", "outDir": "./dist", "typeRoots": [ diff --git a/types/esquery.d.ts b/types/esquery.d.ts index 167c6cf..9e8fb33 100644 --- a/types/esquery.d.ts +++ b/types/esquery.d.ts @@ -1,4 +1,4 @@ -declare module 'esquery' { +declare module 'esquery-scope' { export function parse (selector: string): any; export function match (ast: any, selector: string): Array; }