├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── eslint.config.js
├── index.d.ts
├── index.js
├── package.json
└── spec
├── acorn.spec.js
├── all-browser-tests.js
├── babel-parser.spec.js
├── espree.spec.js
├── esprima.spec.js
├── index-exports.spec.js
├── meriyah.spec.js
├── oxc-parser.spec.js
└── with-parser.js
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | test:
13 | name: Nodejs ${{ matrix.node_version }} on ${{ matrix.os }}
14 | runs-on: ${{ matrix.os }}
15 | strategy:
16 | matrix:
17 | node_version: [18, 20]
18 | os: [ubuntu-latest, windows-latest, macOS-latest]
19 |
20 | steps:
21 | - uses: actions/checkout@v4
22 | - name: Use Node.js ${{ matrix.node_version }}
23 | uses: actions/setup-node@v4
24 | with:
25 | node-version: ${{ matrix.node_version }}
26 | - run: npm install
27 | - run: npm run build --if-present
28 | - run: xvfb-run -a npm test
29 | if: runner.os == 'Linux'
30 | - run: npm test
31 | if: runner.os != 'Linux'
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /npm-debug.*
2 | /yarn-debug.log*
3 | /yarn-error.log*
4 | /node_modules/
5 | /yarn.lock
6 | /package-lock.json
7 | /test.js
8 |
9 | # Mac OS X
10 | .DS_Store
11 |
12 | # direnv
13 | .envrc
14 |
15 | # vim
16 | *~
17 |
18 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # [1.2.0](https://github.com/dumberjs/ast-matcher/compare/v1.1.1...v1.2.0) (2024-05-12)
2 |
3 |
4 | ### Features
5 |
6 | * add support of oxc-parser ([54d0970](https://github.com/dumberjs/ast-matcher/commit/54d09706f7790a2a10c0cd5d3480551de6a396fb))
7 |
8 |
9 |
10 | ## [1.1.1](https://github.com/dumberjs/ast-matcher/compare/v1.1.0...v1.1.1) (2019-08-30)
11 |
12 |
13 | ### Bug Fixes
14 |
15 | * fix @babel/parser compatibility ([3012323](https://github.com/dumberjs/ast-matcher/commit/3012323))
16 |
17 |
18 |
19 | # [1.1.0](https://github.com/dumberjs/ast-matcher/compare/v1.0.5...v1.1.0) (2019-08-30)
20 |
21 |
22 | ### Features
23 |
24 | * be more friendly to babel parser ([ce82918](https://github.com/dumberjs/ast-matcher/commit/ce82918))
25 |
26 |
27 |
28 | ## [1.0.5](https://github.com/dumberjs/ast-matcher/compare/v1.0.4...v1.0.5) (2019-08-29)
29 |
30 |
31 | ### Bug Fixes
32 |
33 | * fix compatibility issue with @babel/parser ([1de4b92](https://github.com/dumberjs/ast-matcher/commit/1de4b92))
34 |
35 |
36 |
37 | ## [1.0.4](https://github.com/dumberjs/ast-matcher/compare/v1.0.3...v1.0.4) (2019-07-13)
38 |
39 |
40 | ### Features
41 |
42 | * improve TypeScript typing ([1da1d3b](https://github.com/dumberjs/ast-matcher/commit/1da1d3b))
43 |
44 |
45 |
46 | ## [1.0.3](https://github.com/dumberjs/ast-matcher/compare/v1.0.2...v1.0.3) (2019-07-04)
47 |
48 |
49 | ### Bug Fixes
50 |
51 | * support matching class body with __anl ([d1d215c](https://github.com/dumberjs/ast-matcher/commit/d1d215c))
52 |
53 |
54 |
55 | ## [1.0.2](https://github.com/dumberjs/ast-matcher/compare/v1.0.1...v1.0.2) (2018-12-05)
56 |
57 |
58 |
59 |
60 | ## [1.0.1](https://github.com/huochunpeng/ast-matcher/compare/v1.0.0...v1.0.1) (2018-10-03)
61 |
62 |
63 |
64 |
65 | # [1.0.0](https://github.com/huochunpeng/ast-matcher/compare/v0.1.6...v1.0.0) (2018-10-03)
66 |
67 |
68 |
69 |
70 | ## [0.1.6](https://github.com/huochunpeng/ast-matcher/compare/v0.1.5...v0.1.6) (2018-04-10)
71 |
72 |
73 | ### Bug Fixes
74 |
75 | * ignore raw, in order to match "foo" with 'foo'. ([64d4b38](https://github.com/huochunpeng/ast-matcher/commit/64d4b38))
76 |
77 |
78 |
79 |
80 | ## [0.1.5](https://github.com/huochunpeng/ast-matcher/compare/v0.1.4...v0.1.5) (2018-04-07)
81 |
82 |
83 | ### Features
84 |
85 | * export traverse function. ([dd56084](https://github.com/huochunpeng/ast-matcher/commit/dd56084))
86 |
87 |
88 |
89 |
90 | ## [0.1.4](https://github.com/huochunpeng/ast-matcher/compare/v0.1.3...v0.1.4) (2018-04-07)
91 |
92 |
93 | ### Features
94 |
95 | * export ensureParsed function. ([4bcd3fd](https://github.com/huochunpeng/ast-matcher/commit/4bcd3fd))
96 |
97 |
98 |
99 |
100 | ## [0.1.3](https://github.com/huochunpeng/ast-matcher/compare/v0.1.2...v0.1.3) (2018-04-05)
101 |
102 |
103 | ### Features
104 |
105 | * basic TypeScript support. ([4f61b1c](https://github.com/huochunpeng/ast-matcher/commit/4f61b1c))
106 |
107 |
108 |
109 |
110 | ## [0.1.2](https://github.com/huochunpeng/ast-matcher/compare/v0.1.1...v0.1.2) (2018-04-04)
111 |
112 |
113 |
114 |
115 | ## [0.1.1](https://github.com/huochunpeng/ast-matcher/compare/v0.1.0...v0.1.1) (2018-04-04)
116 |
117 |
118 | ### Bug Fixes
119 |
120 | * avoid missing js feature on nodejs v4. ([3074d4a](https://github.com/huochunpeng/ast-matcher/commit/3074d4a))
121 | * try skip cherow test on nodejs v4. ([5ba1637](https://github.com/huochunpeng/ast-matcher/commit/5ba1637))
122 |
123 |
124 |
125 |
126 | # 0.1.0 (2018-04-04)
127 |
128 |
129 | ### Features
130 |
131 | * extracted ast-matcher from github.com/huochunpeng/cli ([96aa8de](https://github.com/huochunpeng/ast-matcher/commit/96aa8de))
132 |
133 |
134 |
135 |
136 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 - 2019 Chunpeng Huo.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ast-matcher 
2 |
3 | Create pattern based AST matcher function. So you don't need to be an AST master in order to do static code analysis for JavaScript.
4 |
5 |
6 | ```
7 | npm install ast-matcher
8 | ```
9 |
10 | This tool can be used in `Node.js` or browser. You may need some polyfill for missing JavaScript features in very old browsers.
11 |
12 | ## First, choose a parser
13 |
14 | Beware `ast-matcher` doesn't install a parser for you. You need to manually install one.
15 |
16 | We support any parser compatible with [ESTree spec](https://github.com/estree/estree). Here are some popular ones:
17 |
18 | - [acorn](https://github.com/acornjs/acorn)
19 | - [@babel/parser](https://github.com/babel/babel/tree/master/packages/babel-parser) with `estree` plugin
20 | - [espree](https://github.com/eslint/espree)
21 | - [esprima](https://github.com/jquery/esprima/)
22 | - [meriyah](https://github.com/meriyah/meriyah)
23 |
24 | Take `esprima` for example, you need to use `setParser` to hook it up for `ast-matcher`.
25 |
26 | ```js
27 | const esprima = require('esprima');
28 | const astMatcher = require('ast-matcher');
29 | // with es6, import astMatcher, { depFinder } from 'ast-matcher';
30 |
31 | astMatcher.setParser(esprima.parse);
32 | // or pass options to esprima
33 | astMatcher.setParser(function(contents) {
34 | return esprima.parse(contents, {jsx: true});
35 | });
36 | ```
37 |
38 | Beware `@babel/parser` needs `estree` plugin
39 | ```js
40 | const parser = require('@babel/parser');
41 | const astMatcher = require('ast-matcher');
42 |
43 | astMatcher.setParser(function(contents) {
44 | return parser.parse(contents, {
45 | // ... other options
46 | plugins: [
47 | // ... other plugins
48 | 'estree'
49 | ]
50 | });
51 | });
52 | ```
53 |
54 | For TypeScript user, use:
55 |
56 | ```js
57 | import * as astMatcher from 'ast-matcher';`
58 | let { depFinder } = astMatcher;
59 | ```
60 |
61 | ## API doc for two main functions: `astMatcher` and `depFinder`.
62 |
63 | ### `astMatcher`
64 |
65 | Pattern matching using AST on JavaScript source code.
66 |
67 | ```js
68 | const matcher = astMatcher('__any.method(__str_foo, [__arr_opts])')
69 | matcher('au.method("a", ["b", "c"]); jq.method("d", ["e"])');
70 | // => [
71 | // {match: {foo: "a", opts: ["b", "c"]}, node: }
72 | // {match: {foo: "d", opts: ["e"]}, node: }
73 | // ]
74 | ```
75 |
76 | `astMatcher` takes a `pattern` to be matched. The pattern can only be single statement, not multiple statements. Generates a function that:
77 |
78 | * takes source code string (or estree syntax tree) as input,
79 | * produces matched result or undefined.
80 |
81 | Support following match terms in pattern:
82 |
83 | * `__any` matches any single node, but no extract
84 | * `__anl` matches array of nodes, but no extract
85 | * `__str` matches string literal, but no extract
86 | * `__arr` matches array of partial string literals, but no extract
87 | * `__any_aName` matches single node, return `{aName: node}`
88 | * `__anl_aName` matches array of nodes, return `{aName: array_of_nodes}`
89 | * `__str_aName` matches string literal, return `{aName: value}`
90 | * `__arr_aName` matches array, extract string literals, return `{aName: [values]}`
91 |
92 | > `__arr`, and `__arr_aName` can match partial string array. `[foo, "foo", "bar", lorem] => ["foo", "bar"]`
93 |
94 | > use `method(__anl)` or `method(__arr_a)` to match `method(a, "b");`
95 |
96 | > use `method([__anl])` or `method([__arr_a])` to match `method([a, "b"]);`
97 |
98 | ### `depFinder`
99 |
100 | Dependency analysis for dummies, this is a high level api to simplify the usage of `astMatcher`.
101 |
102 | ```js
103 | const depFinder = astMatcher.depFinder;
104 | const finder = depFinder('a(__dep)', '__any.globalResources([__deps])');
105 | finder('a("a"); a("b"); config.globalResources(["./c", "./d"])');
106 | // => ['a', 'b', './c', './d']
107 | ```
108 |
109 | `depFinder` takes multiple patterns to match, instead of using `__str_`/`__arr`, use `__dep` and `__deps` to match string and partial string array. Generates a function that:
110 |
111 | * takes source code string (or estree syntax tree) as input,
112 | * produces an array of string matched, or empty array.
113 |
114 | ## Examples
115 |
116 | #### 1. find AMD dependencies
117 |
118 | Beware AMD module could be wrapped commonjs module, you need to remove `['require', 'exports', 'module']` from the result.
119 |
120 | ```js
121 | const amdFind = depFinder(
122 | 'define([__deps], __any)', // anonymous module
123 | 'define(__str, [__deps], __any)' // named module
124 | );
125 | const deps = amdFind(amdJsFileContent_or_parsed_ast_tree);
126 | ```
127 |
128 | #### 2. find CommonJS dependencies
129 |
130 | ```js
131 | const cjsFind = depFinder('require(__dep)');
132 | const deps = cjsFind(cjsJsFileContent_or_parsed_ast_tree);
133 | ```
134 |
135 | #### 3. match `if` statement
136 |
137 | ```js
138 | const matcher = astMatcher('if ( __any_condition ) { __anl_body }');
139 | const m = matcher(code_or_parsed_ast_tree);
140 | // => [
141 | // {
142 | // match: { condition: a_node, body: array_of_nodes },
143 | // node: if_statement_node
144 | // },
145 | // ...
146 | // ]
147 | ```
148 |
149 | #### 4. match `if-else` statement
150 |
151 | ```js
152 | const matcher = astMatcher('if ( __any_condition ) { __anl_ifBody } else { __anl_elseBody }');
153 | const m = matcher(code_or_parsed_ast_tree);
154 | // => [
155 | // {
156 | // match: { condition: a_node, ifBody: array_of_nodes, elseBody: array_of_nodes },
157 | // node: if_else_statement_node
158 | // },
159 | // ...
160 | // ]
161 | ```
162 |
163 | #### 5. find [`Aurelia`](http://aurelia.io) framework's `PLATFORM.moduleName()` dependencies
164 |
165 | ```js
166 | const auJsDepFinder = depFinder(
167 | 'PLATFORM.moduleName(__dep)',
168 | '__any.PLATFORM.moduleName(__dep)',
169 | 'PLATFORM.moduleName(__dep, __any)',
170 | '__any.PLATFORM.moduleName(__dep, __any)'
171 | );
172 | const deps = auJsDepFinder(auCode_or_parsed_ast_tree);
173 | ```
174 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | const js = require('@eslint/js');
2 | const globals = require('globals');
3 |
4 | module.exports = [
5 | js.configs.recommended,
6 | {
7 | rules: {
8 | "no-prototype-builtins": "off"
9 | },
10 | languageOptions: {
11 | globals: {
12 | ...globals.node
13 | }
14 | }
15 | }
16 | ];
17 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | export = astMatcher;
2 |
3 | interface MatchedResult {
4 | match: any,
5 | node: any
6 | }
7 |
8 | declare function astMatcher(pattern: string | any): ((code: string | any) => MatchedResult[] | undefined);
9 |
10 | declare namespace astMatcher {
11 | export function depFinder(...patterns: string[]): ((code: string | any) => string[]);
12 | export function setParser(parser: ((code: string) => any)): void;
13 | }
14 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const STOP = false;
3 | const SKIP_BRANCH = 1;
4 |
5 | // ignore position info, and raw
6 | const IGNORED_KEYS = ['start', 'end', 'loc', 'location', 'locations', 'line', 'column', 'range', 'ranges', 'raw', 'extra'];
7 |
8 | let _parser = function() {
9 | throw new Error('No parser set, you need to set parser before use astMatcher. For instance, astMatcher.setParser(esprima.parse)');
10 | };
11 |
12 | function setParser(p) {
13 | if (typeof p !== 'function') {
14 | throw new Error("Input parser must be a function that takes JavaScript contents as input, produce a estree compliant syntax tree object.")
15 | }
16 |
17 | _parser = function(contents) {
18 | let node = p(contents);
19 | // To be friendly to @babel/parser
20 | if (node.type === 'File' && node.program) {
21 | node = node.program;
22 | }
23 | return node;
24 | };
25 | }
26 |
27 | // From an esprima example for traversing its ast.
28 | // modified to support branch skip.
29 | function traverse(object, visitor) {
30 | let child;
31 | if (!object) return;
32 |
33 | let r = visitor.call(null, object);
34 | if (r === STOP) return STOP; // stop whole traverse immediately
35 | if (r === SKIP_BRANCH) return; // skip going into AST branch
36 |
37 | for (let i = 0, keys = Object.keys(object); i < keys.length; i++) {
38 | let key = keys[i];
39 | if (IGNORED_KEYS.indexOf(key) !== -1) continue;
40 |
41 | child = object[key];
42 | if (typeof child === 'object' && child !== null) {
43 | if (traverse(child, visitor) === STOP) {
44 | return STOP;
45 | }
46 | }
47 | }
48 | }
49 |
50 | const ANY = 1;
51 | const ANL = 2;
52 | const STR = 3;
53 | const ARR = 4;
54 |
55 | function matchTerm(pattern) {
56 | let possible;
57 | if (pattern.type === 'Identifier') {
58 | possible = pattern.name.toString();
59 | } else if (pattern.type === 'ExpressionStatement' &&
60 | pattern.expression.type === 'Identifier') {
61 | possible = pattern.expression.name.toString();
62 | } else if ((pattern.type === 'FieldDefinition' || pattern.type === 'ClassProperty') &&
63 | pattern.key.type === 'Identifier') {
64 | possible = pattern.key.name.toString();
65 | }
66 |
67 | if (!possible || !possible.startsWith('__')) return;
68 |
69 | let type;
70 | if (possible === '__any' || possible.startsWith('__any_')) {
71 | type = ANY;
72 | } else if (possible === '__anl' || possible.startsWith('__anl_')) {
73 | type = ANL;
74 | } else if (possible === '__str' || possible.startsWith('__str_')) {
75 | type = STR;
76 | } else if (possible === '__arr' || possible.startsWith('__arr_')) {
77 | type = ARR;
78 | }
79 |
80 | if (type) return {type: type, name: possible.slice(6)};
81 | }
82 |
83 | /**
84 | * Extract info from a partial estree syntax tree, see astMatcher for pattern format
85 | * @param pattern The pattern used on matching
86 | * @param part The target partial syntax tree
87 | * @return Returns named matches, or false.
88 | */
89 | function extract(pattern, part) {
90 | if (!pattern) throw new Error('missing pattern');
91 | // no match
92 | if (!part) return STOP;
93 |
94 | let term = matchTerm(pattern);
95 | if (term) {
96 | // if single __any
97 | if (term.type === ANY) {
98 | if (term.name) {
99 | // if __any_foo
100 | // get result {foo: astNode}
101 | let r = {};
102 | r[term.name] = part;
103 | return r;
104 | }
105 | // always match
106 | return {};
107 |
108 | // if single __str_foo
109 | } else if (term.type === STR) {
110 | if (part.type === 'Literal' || part.type === 'StringLiteral') {
111 | if (term.name) {
112 | // get result {foo: value}
113 | let r = {};
114 | r[term.name] = part.value;
115 | return r;
116 | }
117 | // always match
118 | return {};
119 | }
120 | // no match
121 | return STOP;
122 | }
123 | }
124 |
125 |
126 | if (Array.isArray(pattern)) {
127 | // no match
128 | if (!Array.isArray(part)) return STOP;
129 |
130 | if (pattern.length === 1) {
131 | let arrTerm = matchTerm(pattern[0]);
132 | if (arrTerm) {
133 | // if single __arr_foo
134 | if (arrTerm.type === ARR) {
135 | // find all or partial Literals in an array
136 | let arr = part.filter(function(it) { return it.type === 'Literal' || it.type === 'StringLiteral'; })
137 | .map(function(it) { return it.value; });
138 | if (arr.length) {
139 | if (arrTerm.name) {
140 | // get result {foo: array}
141 | let r = {};
142 | r[arrTerm.name] = arr;
143 | return r;
144 | }
145 | // always match
146 | return {};
147 | }
148 | // no match
149 | return STOP;
150 | } else if (arrTerm.type === ANL) {
151 | if (arrTerm.name) {
152 | // get result {foo: nodes array}
153 | let r = {};
154 | r[arrTerm.name] = part;
155 | return r;
156 | }
157 |
158 | // always match
159 | return {};
160 | }
161 | }
162 | }
163 |
164 | if (pattern.length !== part.length) {
165 | // no match
166 | return STOP;
167 | }
168 | }
169 |
170 | let allResult = {};
171 |
172 | for (let i = 0, keys = Object.keys(pattern); i < keys.length; i++) {
173 | let key = keys[i];
174 | if (IGNORED_KEYS.indexOf(key) !== -1) continue;
175 |
176 | let nextPattern = pattern[key];
177 | let nextPart = part[key];
178 |
179 | if (!nextPattern || typeof nextPattern !== 'object') {
180 | // primitive value. string or null
181 | if (nextPattern === nextPart) continue;
182 |
183 | // no match
184 | return STOP;
185 | }
186 |
187 | const result = extract(nextPattern, nextPart);
188 | // no match
189 | if (result === STOP) return STOP;
190 | if (result) Object.assign(allResult, result);
191 | }
192 |
193 | return allResult;
194 | }
195 |
196 | /**
197 | * Compile a pattern into estree syntax tree
198 | * @param pattern The pattern used on matching, can be a string or estree node
199 | * @return Returns an estree node to be used as pattern in extract(pattern, part)
200 | */
201 | function compilePattern(pattern) {
202 | let exp = ensureParsed(pattern);
203 |
204 | if (exp.type !== 'Program' || !exp.body) {
205 | throw new Error(`Not a valid expression: "${pattern}".`);
206 | }
207 |
208 | if (exp.body.length === 0) {
209 | throw new Error(`There is no statement in pattern "${pattern}".`);
210 | }
211 |
212 | if (exp.body.length > 1) {
213 | throw new Error(`Multiple statements is not supported "${pattern}".`);
214 | }
215 |
216 | exp = exp.body[0];
217 | // get the real expression underneath
218 | if (exp.type === 'ExpressionStatement') exp = exp.expression;
219 | return exp;
220 | }
221 |
222 | function ensureParsed(codeOrNode) {
223 | // bypass parsed node
224 | if (codeOrNode && codeOrNode.type) {
225 | // To be friendly to @babel/parser
226 | if (codeOrNode.type === 'File' && codeOrNode.program) {
227 | return codeOrNode.program;
228 | }
229 | return codeOrNode;
230 | }
231 | return _parser(codeOrNode);
232 | }
233 |
234 | /**
235 | * Pattern matching using AST on JavaScript source code
236 | * @param pattern The pattern to be matched
237 | * @return Returns a function that takes source code string (or estree syntax tree) as input, produces matched result or undefined.
238 | *
239 | * __any matches any single node, but no extract
240 | * __anl matches array of nodes, but no extract
241 | * __str matches string literal, but no extract
242 | * __arr matches array of partial string literals, but no extract
243 | * __any_aName matches single node, return {aName: node}
244 | * __anl_aName matches array of nodes, return {aName: array_of_nodes}
245 | * __str_aName matches string literal, return {aName: value}
246 | * __arr_aName matches array, extract string literals, return {aName: [values]}
247 | *
248 | * note: __arr_aName can match partial array
249 | * [foo, "foo", lorem, "bar", lorem] => ["foo", "bar"]
250 | *
251 | * note: __anl, and __arr_*
252 | * use method(__anl) or method(__arr_a) to match method(a, "b");
253 | * use method([__anl]) or method([__arr_a]) to match method([a, "b"]);
254 | *
255 | * Usage:
256 | * let m = astMatcher('__any.method(__str_foo, [__arr_opts])');
257 | * m('au.method("a", ["b", "c"]); jq.method("d", ["e"])');
258 | *
259 | * => [
260 | * {match: {foo: "a", opts: ["b", "c"]}, node: }
261 | * {match: {foo: "d", opts: ["e"]}, node: }
262 | * ]
263 | */
264 | function astMatcher(pattern) {
265 | let pat = compilePattern(pattern);
266 |
267 | return function(jsStr) {
268 | let node = ensureParsed(jsStr);
269 | let matches = [];
270 |
271 | traverse(node, function (n) {
272 | let m = extract(pat, n);
273 | if (m) {
274 | matches.push({
275 | match: m,
276 | node: n // this is the full matching node
277 | });
278 | // found a match, don't go deeper on this tree branch
279 | // return SKIP_BRANCH;
280 | // don't skip branch in order to catch both .m1() ad .m2()
281 | // astMater('__any.__any_m()')('a.m1().m2()')
282 | }
283 | });
284 |
285 | return matches.length ? matches : undefined;
286 | };
287 | }
288 |
289 | /**
290 | * Dependency analysis for dummies, this is a high level api to simplify the usage of astMatcher
291 | * @param arguments Multiple patterns to match, instead of using __str_/__arr_,
292 | * use __dep and __deps to match string and partial string array.
293 | * @return Returns a function that takes source code string (or estree syntax tree) as input, produces an array of string matched, or empty array.
294 | *
295 | * Usage:
296 | * let f = depFinder('a(__dep)', '__any.globalResources([__deps])');
297 | * f('a("a"); a("b"); config.globalResources(["./c", "./d"])');
298 | *
299 | * => ['a', 'b', './c', './d']
300 | */
301 | function depFinder() {
302 | if (arguments.length === 0) {
303 | throw new Error('No patterns provided.');
304 | }
305 |
306 | let seed = 0;
307 |
308 | let patterns = Array.prototype.map.call(arguments, function (p) {
309 | // replace __dep and __deps into
310 | // __str_1, __str_2, __arr_3
311 |
312 | // wantArr is the result of (s?)
313 | return compilePattern(p.replace(/__dep(s?)/g, function (m, wantArr) {
314 | return (wantArr ? '__arr_' : '__str_') + (++seed);
315 | }))
316 | });
317 |
318 | let len = patterns.length;
319 |
320 | return function(jsStr) {
321 | let node = ensureParsed(jsStr);
322 |
323 | let deps = [];
324 |
325 | // directly use extract() instead of astMatcher()
326 | // for efficiency
327 | traverse(node, function (n) {
328 | for (let i = 0; i < len; i += 1) {
329 | let result = extract(patterns[i], n);
330 | if (result) {
331 | // result is like {"1": "dep1", "2": ["dep2", "dep3"]}
332 | // we only want values
333 | Object.keys(result).forEach(function (k) {
334 | let d = result[k];
335 | if (typeof d === 'string') deps.push(d);
336 | else deps.push.apply(deps, d);
337 | });
338 |
339 | // found a match, don't try other pattern
340 | break;
341 | }
342 | }
343 | });
344 |
345 | return deps;
346 | };
347 | }
348 |
349 | module.exports = astMatcher;
350 | module.exports.setParser = setParser;
351 | module.exports.ensureParsed = ensureParsed;
352 | module.exports.extract = extract;
353 | module.exports.compilePattern = compilePattern;
354 | module.exports.depFinder = depFinder;
355 | module.exports.STOP = STOP;
356 | module.exports.SKIP_BRANCH = SKIP_BRANCH;
357 | module.exports.traverse = traverse;
358 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ast-matcher",
3 | "version": "1.2.0",
4 | "description": "Create pattern based AST matcher function",
5 | "main": "index.js",
6 | "types": "index.d.ts",
7 | "files": [
8 | "index.js",
9 | "index.d.ts"
10 | ],
11 | "scripts": {
12 | "lint": "eslint index.js spec",
13 | "preversion": "npm test",
14 | "version": "standard-changelog && git add CHANGELOG.md",
15 | "postversion": "git push && git push --tags && npm publish",
16 | "pretest": "npm run lint",
17 | "browser-test": "browserify spec/all-browser-tests.js | browser-do --tap",
18 | "nodejs-test": "tape \"spec/*.spec.js\"",
19 | "test": "npm run nodejs-test && npm run browser-test"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "https://github.com/dumberjs/ast-matcher.git"
24 | },
25 | "keywords": [
26 | "ast",
27 | "parser",
28 | "match",
29 | "esprima",
30 | "acorn",
31 | "espree",
32 | "meriyah"
33 | ],
34 | "author": "Chunpeng Huo",
35 | "license": "MIT",
36 | "bugs": {
37 | "url": "https://github.com/dumberjs/ast-matcher/issues"
38 | },
39 | "homepage": "https://github.com/dumberjs/ast-matcher#readme",
40 | "devDependencies": {
41 | "@babel/parser": "^7.24.5",
42 | "acorn": "^8.11.3",
43 | "browser-do": "^5.0.0",
44 | "browserify": "^17.0.0",
45 | "cherow": "^1.6.9",
46 | "eslint": "^9.2.0",
47 | "espree": "^10.0.1",
48 | "esprima": "^4.0.1",
49 | "meriyah": "^4.4.2",
50 | "oxc-parser": "^0.9.0",
51 | "standard-changelog": "^6.0.0",
52 | "tape": "^5.7.5"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/spec/acorn.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const acorn = require('acorn');
3 | const withParser = require('./with-parser');
4 |
5 | withParser('acorn', f => acorn.parse(f, {ranges: true, locations: true}));
6 |
--------------------------------------------------------------------------------
/spec/all-browser-tests.js:
--------------------------------------------------------------------------------
1 | require('./acorn.spec.js');
2 | require('./babel-parser.spec.js');
3 | require('./espree.spec.js');
4 | require('./esprima.spec.js');
5 | require('./index-exports.spec.js');
6 | require('./meriyah.spec.js');
7 | // require('./oxc-parser.spec.js');
--------------------------------------------------------------------------------
/spec/babel-parser.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const parser = require('@babel/parser');
3 | const withParser = require('./with-parser');
4 |
5 | withParser('@babel/parser', code =>
6 | parser.parse(code, {sourceType: 'module', plugins: [
7 | 'jsx',
8 | 'typescript',
9 | 'asyncGenerators',
10 | 'bigInt',
11 | 'classProperties',
12 | 'classPrivateProperties',
13 | 'classPrivateMethods',
14 | 'decorators-legacy',
15 | // ['decorators', {'decoratorsBeforeExport': true}],
16 | 'doExpressions',
17 | 'dynamicImport',
18 | 'exportDefaultFrom',
19 | 'exportNamespaceFrom',
20 | 'functionBind',
21 | 'functionSent',
22 | 'importMeta',
23 | 'logicalAssignment',
24 | 'nullishCoalescingOperator',
25 | 'numericSeparator',
26 | 'objectRestSpread',
27 | 'optionalCatchBinding',
28 | 'optionalChaining',
29 | 'partialApplication',
30 | // ['pipelineOperator', {proposal: 'minimal'}],
31 | 'throwExpressions',
32 | 'estree'
33 | ]})
34 | );
35 |
--------------------------------------------------------------------------------
/spec/espree.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const espree = require('espree');
3 | const withParser = require('./with-parser');
4 |
5 | withParser('espree', espree.parse);
6 |
--------------------------------------------------------------------------------
/spec/esprima.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const esprima = require('esprima');
3 | const withParser = require('./with-parser');
4 |
5 | withParser('esprima', esprima.parse);
6 |
--------------------------------------------------------------------------------
/spec/index-exports.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const test = require('tape');
3 |
4 | const astMatcher = require('../index');
5 |
6 | test('got exports', t => {
7 | t.ok(astMatcher);
8 | t.ok(astMatcher.setParser);
9 | t.ok(astMatcher.ensureParsed);
10 | t.ok(astMatcher.extract);
11 | t.ok(astMatcher.compilePattern);
12 | t.ok(astMatcher.depFinder);
13 | t.ok(astMatcher.hasOwnProperty('STOP'));
14 | t.ok(astMatcher.hasOwnProperty('SKIP_BRANCH'));
15 | t.ok(astMatcher.traverse);
16 | t.end();
17 | });
18 |
--------------------------------------------------------------------------------
/spec/meriyah.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const meriyah = require('meriyah');
3 | const withParser = require('./with-parser');
4 |
5 | withParser('meriyah', meriyah.parse);
6 |
--------------------------------------------------------------------------------
/spec/oxc-parser.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const { parseSync } = require('oxc-parser');
3 | const withParser = require('./with-parser');
4 |
5 | withParser('oxc-parser', code => JSON.parse(parseSync(code).program));
6 |
--------------------------------------------------------------------------------
/spec/with-parser.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const test = require('tape');
3 |
4 | const astMatcher = require('../index');
5 | const extract = astMatcher.extract;
6 | const compilePattern = astMatcher.compilePattern;
7 | const depFinder = astMatcher.depFinder;
8 |
9 | const extractTest = function(pattern, part) {
10 | return extract(compilePattern(pattern), compilePattern(part));
11 | };
12 |
13 | let checkedMissing;
14 |
15 | module.exports = function (parserName, parser) {
16 | function testP(title, cb) {
17 | test('[' + parserName + '] ' + title, t => {
18 | astMatcher.setParser(parser);
19 | cb(t);
20 | });
21 | }
22 |
23 | if (!checkedMissing) {
24 | checkedMissing = true;
25 |
26 | test('missing parser throws error', t => {
27 | t.throws(() => astMatcher('a()'));
28 | t.end();
29 | });
30 | }
31 |
32 | testP('compilePattern understands extree node', t => {
33 | let node = parser('a = 1');
34 | t.equal(compilePattern(node).type, 'AssignmentExpression');
35 | t.end();
36 | });
37 |
38 | testP('compilePattern returns single expression node, unwrap ExpressionStatement', t => {
39 | let p = compilePattern('a');
40 | t.equal(p.type, 'Identifier');
41 | t.equal(p.name, 'a');
42 | t.end();
43 | });
44 |
45 | testP('compilePattern rejects multi statements', t => {
46 | t.throws(() => compilePattern('a; b = 1'));
47 | t.end();
48 | });
49 |
50 | testP('compilePattern rejects empty pattern', t => {
51 | t.throws(() => compilePattern('// nope'));
52 | t.end();
53 | });
54 |
55 | testP('compilePattern rejects syntax err', t => {
56 | t.throws(() => compilePattern('a+'));
57 | t.end();
58 | });
59 |
60 | testP('extract bare term has limited support', t => {
61 | t.deepEqual(extractTest('__any', 'a(foo)'), {});
62 | t.equal(extractTest('__anl', 'foo,bar'), false);
63 | t.deepEqual(extractTest('(__str)', '("foo")'), {});
64 | t.deepEqual(extractTest('(__str_a)', '("foo")'), {a: 'foo'});
65 | t.end();
66 | });
67 |
68 | testP('extract __any matches any node but no extract', t => {
69 | t.deepEqual(extractTest('a(__any)', 'a(foo)'), {});
70 | t.deepEqual(extractTest('a(__any)', 'a("foo")'), {});
71 | t.deepEqual(extractTest('a(__any,__any)', 'a("foo", "bar")'), {});
72 | t.end();
73 | });
74 |
75 | testP('extract __any_name matches any node', t => {
76 | let r = extractTest('a(__any_a)', 'a(foo)');
77 | t.deepEqual(Object.keys(r), ['a']);
78 | t.equal(r.a.type, 'Identifier');
79 | t.equal(r.a.name, 'foo');
80 |
81 | r = extractTest('a(__any_a)', 'a("foo")');
82 | t.deepEqual(Object.keys(r), ['a']);
83 | t.ok(['Literal', 'StringLiteral'].includes(r.a.type));
84 | t.equal(r.a.value, 'foo');
85 |
86 | r = extractTest('a(__any_a,__any_b)', 'a("foo", "bar")');
87 | t.deepEqual(Object.keys(r).sort(), ['a', 'b']);
88 | t.ok(['Literal', 'StringLiteral'].includes(r.a.type));
89 | t.equal(r.a.value, 'foo');
90 | t.ok(['Literal', 'StringLiteral'].includes(r.b.type));
91 | t.equal(r.b.value, 'bar');
92 | t.end();
93 | });
94 |
95 | testP('extract __anl matches nodes array, but no extract', t => {
96 | let r = extractTest('a(__anl)', 'a(foo, bar)');
97 | t.deepEqual(r, {});
98 | r = extractTest('a([__anl])', 'a(foo, bar)');
99 | t.equal(r, false);
100 |
101 | r = extractTest('a([__anl])', 'a(foo, bar)');
102 | t.equal(r, false);
103 | r = extractTest('a([__anl])', 'a([foo, bar])');
104 | t.deepEqual(r, {});
105 | t.end();
106 | });
107 |
108 | testP('extract __anl_name matches nodes array', t => {
109 | let r = extractTest('a(__anl_a)', 'a(foo, bar)');
110 | t.deepEqual(Object.keys(r), ['a']);
111 | t.equal(r.a.length, 2);
112 | t.equal(r.a[0].type, 'Identifier');
113 | t.equal(r.a[0].name, 'foo');
114 | t.equal(r.a[1].type, 'Identifier');
115 | t.equal(r.a[1].name, 'bar');
116 |
117 | r = extractTest('a(__anl_a)', 'a([foo, bar])');
118 | t.deepEqual(Object.keys(r), ['a']);
119 | t.equal(r.a.length, 1);
120 | t.equal(r.a[0].type, 'ArrayExpression');
121 | t.end();
122 | });
123 |
124 | testP('extract __anl_name matches nodes array case2', t => {
125 | let r = extractTest('a([__anl_a])', 'a([foo, bar])');
126 | t.deepEqual(Object.keys(r), ['a']);
127 | t.equal(r.a.length, 2);
128 | t.equal(r.a[0].type, 'Identifier');
129 | t.equal(r.a[0].name, 'foo');
130 | t.equal(r.a[1].type, 'Identifier');
131 | t.equal(r.a[1].name, 'bar');
132 |
133 | r = extractTest('a([__anl_a])', 'a(foo, bar)');
134 | t.equal(r, false);
135 | t.end();
136 | });
137 |
138 | testP('extract extracts matching string literal', t => {
139 | t.equal(extractTest('a(__str_a)', 'a(foo)'), false);
140 | t.deepEqual(extractTest('a(__str_a)', 'a("foo")'), {a: 'foo'});
141 | t.deepEqual(extractTest('a(__str_a,__str_b)', 'a("foo", "bar")'), {a: 'foo', b: 'bar'});
142 | t.end();
143 | });
144 |
145 | testP('extract matches string literal', t => {
146 | t.equal(extractTest('a(__str)', 'a(foo)'), false);
147 | t.deepEqual(extractTest('a(__str)', 'a("foo")'), {});
148 | t.deepEqual(extractTest('a(__str,__str)', 'a("foo", "bar")'), {});
149 | t.end();
150 | });
151 |
152 | testP('extract extracts matching array string literal', t => {
153 | t.equal(extractTest('a(__arr_a)', 'a(["foo", "bar"])'), false);
154 | t.deepEqual(extractTest('a(__arr_a)', 'a("foo", "bar")'), {a: ['foo', 'bar']});
155 |
156 | t.deepEqual(extractTest('a([__arr_a])', 'a(["foo", "bar"])'), {a: ['foo', 'bar']});
157 | t.equal(extractTest('a([__arr_a])', 'a("foo", "bar")'), false);
158 |
159 | t.deepEqual(extractTest('a([__arr_a])', 'a(["foo", partial, literal, arr, "bar"])'), {a: ['foo', 'bar']});
160 | t.equal(extractTest('a([__arr_a])', 'a([no, literal, arr])'), false);
161 | t.end();
162 | });
163 |
164 | testP('extract matches array string literal', t => {
165 | t.equal(extractTest('a(__arr)', 'a(["foo", "bar"])'), false);
166 | t.deepEqual(extractTest('a(__arr)', 'a("foo", "bar")'), {});
167 |
168 | t.deepEqual(extractTest('a([__arr])', 'a(["foo", "bar"])'), {});
169 | t.equal(extractTest('a([__arr])', 'a("foo", "bar")'), false);
170 |
171 | t.deepEqual(extractTest('a([__arr])', 'a(["foo", partial, literal, arr, "bar"])'), {});
172 | t.equal(extractTest('a([__arr])', 'a([no, literal, arr])'), false);
173 | t.end();
174 | });
175 |
176 | testP('extract extracts matching array string literal, and string literal', t => {
177 | t.equal(extractTest('a(__str_a, __arr_b)', 'a("foo", "bar")'), false);
178 | t.equal(extractTest('a(__str_a, __arr_b)', 'a("foo", ["bar"])'), false);
179 | t.deepEqual(extractTest('a(__str_a, [__arr_b])', 'a("foo", ["bar"])'), {a: 'foo', b: ['bar']});
180 | t.end();
181 | });
182 |
183 | testP('extract supports wildcard', t => {
184 | t.equal(extractTest('__any.a(__str_a, [__arr_b])', 'a("foo", ["bar"])'), false);
185 | t.deepEqual(extractTest('__any.a(__str_a, [__arr_b])', 'bar.a("foo", ["bar"])'), {a: 'foo', b: ['bar']});
186 | t.end();
187 | });
188 |
189 | testP('extract try complex pattern', t => {
190 | t.equal(extractTest(
191 | '(0, __any.noView)([__arr_deps], __str_baseUrl)',
192 | '(0, _aureliaFramework.noView)(["foo", "bar"])'
193 | ), false);
194 | t.deepEqual(extractTest(
195 | '(__any, __any.noView)([__arr_deps], __str_baseUrl)',
196 | '(1, _aureliaFramework.noView)(["./foo", "./bar"], "lorem")'
197 | ), {deps: ['./foo', './bar'], baseUrl: 'lorem'});
198 | t.deepEqual(extractTest(
199 | '(__any, __any.noView)([__arr_deps])',
200 | '(1, _aureliaFramework.noView)(["./foo", "./bar"])'
201 | ), {deps: ['./foo', './bar']});
202 | t.deepEqual(extractTest(
203 | '(__any, __any.useView)(__arr_dep)',
204 | '(1, _aureliaFramework.useView)("./foo.html")'
205 | ), {dep: ['./foo.html']});
206 | t.end();
207 | });
208 |
209 | testP('extact matches string literal without testing raw', t => {
210 | t.deepEqual(extractTest('a("foo")', "a('foo')"), {});
211 | t.end();
212 | });
213 |
214 | testP('astMatcher builds matcher', t => {
215 | t.equal(typeof astMatcher('a(__str_a)'), 'function');
216 | t.end();
217 | });
218 |
219 | testP('matcher built by astMatcher returns undefined on no match', t => {
220 | let m = astMatcher('__any.method(__str_foo, [__arr_opts])');
221 | let r = m('au.method(["b", "c"]); method("d", ["e"])');
222 | t.equal(r, undefined);
223 | t.end();
224 | });
225 |
226 | testP('matcher built by astMatcher accepts both string input or node input', t => {
227 | let m = astMatcher('a(__str_foo)');
228 | t.equal(m('a("foo")').length, 1);
229 | t.equal(m(parser('a("foo")')).length, 1);
230 | t.end();
231 | });
232 |
233 | testP('matcher built by astMatcher returns matches and matching nodes', t => {
234 | let m = astMatcher('__any.method(__str_foo, [__arr_opts])');
235 | let r = m('function test(au, jq) { au.method("a", ["b", "c"]); jq.method("d", ["e"]); }');
236 | t.equal(r.length, 2);
237 |
238 | t.deepEqual(r[0].match, {foo: 'a', opts: ['b', 'c']});
239 | t.equal(r[0].node.type, 'CallExpression');
240 | t.equal(r[0].node.callee.object.name, 'au');
241 | t.deepEqual(r[1].match, {foo: 'd', opts: ['e']});
242 | t.equal(r[1].node.type, 'CallExpression');
243 | t.equal(r[1].node.callee.object.name, 'jq');
244 | t.end();
245 | });
246 |
247 | testP('matcher built by astMatcher returns matching nodes with no named matches', t => {
248 | let m = astMatcher('__any.method(__any, [__anl])');
249 | let r = m('function test(au, jq) { au.method("a", ["b", "c"]); jq.method("d", ["e"]); }');
250 | t.equal(r.length, 2);
251 |
252 | t.deepEqual(r[0].match, {});
253 | t.equal(r[0].node.type, 'CallExpression');
254 | t.equal(r[0].node.callee.object.name, 'au');
255 | t.deepEqual(r[1].match, {});
256 | t.equal(r[1].node.type, 'CallExpression');
257 | t.equal(r[1].node.callee.object.name, 'jq');
258 | t.end();
259 | });
260 |
261 | testP('matcher built by astMatcher complex if statement', t => {
262 | let m = astMatcher('if (__any) { __anl }');
263 | let r = m('if (yes) { a(); b(); }');
264 | t.equal(r.length, 1);
265 |
266 | r = m('if (yes) { a(); b(); } else {}');
267 | t.equal(r, undefined);
268 |
269 | r = m('if (c && d) { a(); b(); }');
270 | t.equal(r.length, 1);
271 | t.end();
272 | });
273 |
274 | testP('matcher built by astMatcher complex pattern', t => {
275 | const m = astMatcher('(__any, __any.noView)([__arr_deps])');
276 | const r = m('(dec = (1, _aureliaFramework.noView)(["./foo", "./bar"]), dec())');
277 | t.ok(r);
278 | t.equal(r.length, 1);
279 | t.deepEqual(r[0].match.deps, ["./foo", "./bar"]);
280 |
281 | const m2 = astMatcher('(__any, __any.useView)(__arr_dep)');
282 | const r2 = m2('(dec = (1, _aureliaFramework.useView)("./foo.html"), dec())');
283 | t.ok(r2);
284 | t.equal(r2.length, 1);
285 | t.deepEqual(r2[0].match.dep, ['./foo.html']);
286 | t.end();
287 | });
288 |
289 | if (parserName === 'cherow') {
290 | testP('matcher built by astMatcher supports class body with __anl', t => {
291 | let m = astMatcher('export class __any_name { __anl_body }');
292 | let r = m(`
293 | export class Foo {
294 | name = 'ok';
295 | bar() {}
296 | get loo() {}
297 | }
298 | `);
299 | t.equal(r.length, 1);
300 | t.equal(r[0].match.name.name, 'Foo');
301 | t.equal(r[0].match.body.length, 3);
302 | t.equal(r[0].match.body[0].key.name, 'name');
303 | t.equal(r[0].match.body[1].key.name, 'bar');
304 | t.equal(r[0].match.body[2].key.name, 'loo');
305 | t.end();
306 | });
307 |
308 | testP('matcher built by astMatcher supports class body with __anl case 2', t => {
309 | let m = astMatcher('class __any_name { __anl }');
310 | let r = m(`
311 | class Foo {
312 | name = 'ok';
313 | bar() {}
314 | get loo() {}
315 | }
316 | `);
317 | t.equal(r.length, 1);
318 | t.equal(r[0].match.name.name, 'Foo');
319 | t.end();
320 | });
321 | }
322 |
323 | testP('matcher built by astMatcher continues to match even after match found', t => {
324 | let m = astMatcher('__any.__any_m()');
325 | let r = m('a.m1().m2()');
326 | t.equal(r.length, 2);
327 | t.deepEqual(r.map(i => i.match.m.name).sort(), ['m1', 'm2']);
328 | t.end();
329 | });
330 |
331 | testP('depFinder rejects empty input', t => {
332 | t.throws(() => depFinder());
333 | t.end();
334 | });
335 |
336 | testP('depFinder complains about wrong exp', t => {
337 | t.throws(() => depFinder('+++'));
338 | t.end();
339 | });
340 |
341 | testP('depFinder returns empty array on no match', t => {
342 | let f = depFinder('a(__dep)');
343 | t.deepEqual(f('fds("a")'), []);
344 | t.end();
345 | });
346 |
347 | testP('depFinder finds matching dep', t => {
348 | let f = depFinder('a(__dep)');
349 | t.deepEqual(f('a("a"); b("b"); b.a("c")'), ['a']);
350 | t.end();
351 | });
352 |
353 | testP('depFinder finds matching dep, accepts esprima node as input', t => {
354 | let f = depFinder('a(__dep)');
355 | t.deepEqual(f(parser('a("a"); b("b"); b.a("c")')), ['a']);
356 | t.end();
357 | });
358 |
359 | testP('depFinder finds matching dep by matching length', t => {
360 | let f = depFinder('a(__dep, __dep)');
361 | t.deepEqual(f('a("a"); a("b", "c"); a("d", "e", "f")'), ['b', 'c']);
362 | t.end();
363 | });
364 |
365 | testP('depFinder finds matching dep with wild card', t => {
366 | let f = depFinder('__any.a(__dep)');
367 | t.deepEqual(f('a("a"); b.a("b"); c["f"].a("c")'), ['b', 'c']);
368 | t.end();
369 | });
370 |
371 | testP('depFinder find matching deps', t => {
372 | let f = depFinder('a(__deps)');
373 | t.deepEqual(f('a("a"); a("b", "c");'), ['a', 'b', 'c']);
374 | t.end();
375 | });
376 |
377 | testP('depFinder accepts multiple patterns', t => {
378 | let f = depFinder('a(__deps)', '__any.a(__deps)');
379 | t.deepEqual(f('fds("a")'), []);
380 | t.deepEqual(
381 | f('a("a"); b("b"); b.a("c"); c.a("d", "e"); a("f", "g")'),
382 | ['a', 'c', 'd', 'e', 'f', 'g']);
383 |
384 | f = depFinder('a(__dep)', '__any.globalResources([__deps])');
385 | t.deepEqual(
386 | f('a("a"); a("b"); config.globalResources(["./c", "./d"])'),
387 | ['a', 'b', './c', './d']);
388 | t.end();
389 | });
390 | }
391 |
--------------------------------------------------------------------------------