├── .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 ![CI](https://github.com/dumberjs/ast-matcher/workflows/CI/badge.svg) 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 | --------------------------------------------------------------------------------