Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(WIP) feat: Add :scope and :root selectors #37

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"esquery": "^1.0.1"
"esquery-scope": "^1.1.0"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunatly I had to keep the esquery-scope in order to be able to run the ci

},
"peerDependencies": {
"typescript": "^3"
Expand Down
21 changes: 17 additions & 4 deletions src/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,28 @@ import { MATCHERS } from './matchers';
import { traverseChildren } from './traverse';
import { TSQueryOptions, TSQuerySelectorNode } from './tsquery-types';

export function match <T extends Node = Node> (node: Node, selector: TSQuerySelectorNode, options: TSQueryOptions = {}): Array<T> {
export function match <T extends Node = Node> (node: Node, selector: TSQuerySelectorNode, scope: Node, options: TSQueryOptions = {}): Array<T> {
const results: Array<T> = [];
if (!selector) {
return results;
}

if (selector.left) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this to be able to go back to the root node when the :root selector is matched.
It's seems incomplete as it will only work when the :root selector is in the left part of the selector AST root.

if (selector.left.type as any === 'root') {
node = getRootNode(node);
}
}

traverseChildren(node, (childNode: Node, ancestry: Array<Node>) => {
if (findMatches(childNode, selector, ancestry, options)) {
if (findMatches(childNode, selector, ancestry, scope, options)) {
results.push(childNode as T);
}
}, options);

return results;
}

export function findMatches (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node> = [], options: TSQueryOptions = {}): boolean {
export function findMatches (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node> = [], scope: Node, options: TSQueryOptions = {}): boolean {
if (!selector) {
return true;
}
Expand All @@ -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;
}
6 changes: 3 additions & 3 deletions src/matchers/child.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Node>): 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<Node>, scope: Node): boolean {
if (findMatches(node, selector.right, ancestry, scope)) {
return findMatches(ancestry[0], selector.left, ancestry.slice(1), scope);
}
return false;
}
4 changes: 2 additions & 2 deletions src/matchers/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ const CLASS_MATCHERS: TSQueryMatchers = {
statement
};

export function classs (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node>, options: TSQueryOptions): boolean {
export function classs (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node>, 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}`);
Expand Down
6 changes: 3 additions & 3 deletions src/matchers/descendant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Node>): boolean {
if (findMatches(node, selector.right, ancestry)) {
export function descendant (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node>, 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;
Expand Down
28 changes: 19 additions & 9 deletions src/matchers/has.ts
Original file line number Diff line number Diff line change
@@ -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<Node>, options: TSQueryOptions): boolean {
export function has (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node>, {}: Node, {}: TSQueryOptions): boolean {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got the :scope selector by moving the traverseChildren function in here. It's probably not the best way to do it.

const collector: Array<Node> = [];
selector.selectors.forEach(childSelector => {
traverseChildren(node, (childNode: Node, ancestry: Array<Node>) => {
if (findMatches(childNode, childSelector, ancestry)) {
const parent = ancestry[0];
let a: Array<Node> = [];
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;

}
4 changes: 4 additions & 0 deletions src/matchers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -29,6 +31,8 @@ export const MATCHERS: TSQueryMatchers = {
identifier,
matches: matches('some'),
not,
root,
scope,
sibling,
wildcard
};
6 changes: 3 additions & 3 deletions src/matchers/matches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Node>) => boolean {
return function (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node>): boolean {
export function matches (modifier: 'some' | 'every'): (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node>, scope: Node) => boolean {
return function (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node>, scope: Node): boolean {
return selector.selectors[modifier](childSelector => {
return findMatches(node, childSelector, ancestry);
return findMatches(node, childSelector, ancestry, scope);
});
};
}
4 changes: 2 additions & 2 deletions src/matchers/not.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Node>): boolean {
export function not (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node>, scope: Node): boolean {
return !selector.selectors.some(childSelector => {
return findMatches(node, childSelector, ancestry);
return findMatches(node, childSelector, ancestry, scope);
});
}
8 changes: 4 additions & 4 deletions src/matchers/nth-child.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Node>): boolean {
return findMatches(node, selector.right, ancestry) &&
export function nthChild (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node>, 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<Node>): boolean {
return findMatches(node, selector.right, ancestry) &&
export function nthLastChild (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node>, scope: Node): boolean {
return findMatches(node, selector.right, ancestry, scope) &&
findNthChild(node, ancestry, (length: number) => length - (selector.index.value as number));
}

Expand Down
5 changes: 5 additions & 0 deletions src/matchers/root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Node } from 'typescript';

export function root ({}: any, {}: any, ancestry: Array<Node>): boolean {
return ancestry.length === 0;
run1t marked this conversation as resolved.
Show resolved Hide resolved
}
5 changes: 5 additions & 0 deletions src/matchers/scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Node } from 'typescript';

export function scope (node: any, {}: any, ancestry: Array<Node>, _scope: Node): boolean {
return _scope ? node === _scope : ancestry.length === 0;
}
20 changes: 10 additions & 10 deletions src/matchers/sibling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Node>): boolean {
return findMatches(node, selector.right, ancestry) &&
export function sibling (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node>, 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<Node>): boolean {
return findMatches(node, selector.right, ancestry) &&
export function adjacent (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node>, 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);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/parse.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
2 changes: 1 addition & 1 deletion src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export function query <T extends Node = Node> (ast: string | Node, selector: str
if (typeof ast === 'string') {
ast = createAST(ast);
}
return match<T>(ast, parse(selector), options);
return match<T>(ast, parse(selector), ast, options);
}
2 changes: 1 addition & 1 deletion src/traverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
4 changes: 2 additions & 2 deletions src/tsquery-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type TSQueryApi = {
<T extends Node = Node> (ast: string | Node, selector: string, options?: TSQueryOptions): Array<T>;
ast (source: string, fileName?: string, scriptKind?: ScriptKind): SourceFile;
map (ast: SourceFile, selector: string, nodeTransformer: TSQueryNodeTransformer, options?: TSQueryOptions): SourceFile;
match <T extends Node = Node> (ast: Node, selector: TSQuerySelectorNode, options?: TSQueryOptions): Array<T>;
match <T extends Node = Node> (ast: Node, selector: TSQuerySelectorNode, node: Node, options?: TSQueryOptions): Array<T>;
parse (selector: string, options?: TSQueryOptions): TSQuerySelectorNode;
project (configFilePath: string): Array<SourceFile>;
query <T extends Node = Node> (ast: string | Node, selector: string, options?: TSQueryOptions): Array<T>;
Expand All @@ -22,7 +22,7 @@ export type TSQueryAttributeOperators = {
[key: string]: TSQueryAttributeOperator
};

export type TSQueryMatcher = (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node>, options: TSQueryOptions) => boolean;
export type TSQueryMatcher = (node: Node, selector: TSQuerySelectorNode, ancestry: Array<Node>, scope: Node, options: TSQueryOptions) => boolean;
export type TSQueryMatchers = {
[key: string]: TSQueryMatcher;
};
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './simple-function';
export * from './simple-program';
export * from './statement';
export * from './jsx';
export * from './nested-functions';
9 changes: 9 additions & 0 deletions test/fixtures/nested-functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const nestedFunctions = `
function a(){
function b(){
return 'b';
}
return 'a';
}

`;
16 changes: 8 additions & 8 deletions test/project.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ import { tsquery } from '../src/index';

describe('tsquery:', () => {
describe('tsquery.project:', () => {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a problem with the tests below. I don't know why but some times when I was launching the test the file.length was incrementing even if I didn't touch the ./tsconfig.json. Do I need to open an issue ?

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');
Expand Down
Loading