ast-search 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/build/file.js +58 -0
- package/build/helpers/nodes.js +149 -0
- package/build/main.js +99 -0
- package/build/output.js +20 -0
- package/build/search.js +71 -0
- package/build/walk.js +54 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# ast-search
|
|
2
|
+
|
|
3
|
+
A (somewhat) limited-case search tool meant to facilitate large-scale refactors.
|
|
4
|
+
|
|
5
|
+
Searches files for a given function or property name, and then determines whether those functions or properties contain a given expression or value.
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
For example, say you have a large number of VueJS single-file-components with `setup` functions that might be improperly accessing the `this.` instance of the file:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
<script>
|
|
13
|
+
...
|
|
14
|
+
export default {
|
|
15
|
+
setup() {
|
|
16
|
+
const store = useStore();
|
|
17
|
+
const dynamicTestValue = computed(() => {
|
|
18
|
+
return this.testValue
|
|
19
|
+
})
|
|
20
|
+
},
|
|
21
|
+
computed: {
|
|
22
|
+
testValue: 0
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If you have hundreds of component files, this would be especially difficult (if not impossible) to find with a regular expression because it would require both searching across multiple lines and knowing the lexical scope of the `setup` function, which would vary widely across each file.
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
`ast-search` leverages [ acornjs ](https://github.com/acornjs/acorn) to construct an Abstract Syntax Tree of each JavaScript file it parses, and then searches relevant paths for both the given top-level function or property as well as the desired search term.
|
|
33
|
+
|
|
34
|
+
For example, say the above VueJS SFC is a file at `src/components/TestValue.vue`. To search this file and determine whether the `setup` function has a `this.` expression, you'd do the following:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
$ ast-search -f ./src/components/TestValue.vue --fn setup -e ThisExpression
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If it finds the expression in the function, it echoes the file path:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
$ ast-search -f ./src/components/TestValue.vue --fn setup -e ThisExpression
|
|
44
|
+
./src/components/TestValue.vue
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Arguments
|
|
48
|
+
|
|
49
|
+
| Flag | Definition |
|
|
50
|
+
| ------------------ | -------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `-f, --file` | the file path to search, relative or absolute |
|
|
52
|
+
| `--function, fn` | the function to search; can be any variation of a function declaration or expression |
|
|
53
|
+
| `-p, --property` | the property to search; expected to be a property on an object |
|
|
54
|
+
| `-m, --multiple` | used for searching function calls, of which a single file may contain several |
|
|
55
|
+
| `-e, --expression` | the type of expression to search for; must be a valid `acornjs` JavaScript expression (list below) |
|
|
56
|
+
| `-d, --debug` | output the full Abstract Syntax Tree to a file called `output.json` |
|
|
57
|
+
|
|
58
|
+
### Supported Expressions
|
|
59
|
+
|
|
60
|
+
| Expression Name | Reference | Common Operators |
|
|
61
|
+
| ------------------------ | ----------------------------------------------------- | ----------------------------- |
|
|
62
|
+
| ArrayExpression | begins an array expression | `[]` |
|
|
63
|
+
| ArrowFunctionExpression | begins an arrow function in any lexical context | `() =>` |
|
|
64
|
+
| AssignmentExpression | equals operator | `=` |
|
|
65
|
+
| AwaitExpression | asynchronous promise resolver | `await` |
|
|
66
|
+
| BinaryExpression | any left + right side operation | `1 + 1` |
|
|
67
|
+
| CallExpression | function calls | `fn()` |
|
|
68
|
+
| ChainExpression | chained property or method accessors | `store.state.someFn()` |
|
|
69
|
+
| ConditionalExpression | ternary expression | `x ? y : z` |
|
|
70
|
+
| FunctionExpression | named, non-assigned functions | `fn() {}` |
|
|
71
|
+
| ImportExpression | import statements | `import x from 'y'` |
|
|
72
|
+
| LogicalExpression | evaluates to a boolean, e.g. if statement expressions | `if (x > y)` |
|
|
73
|
+
| MemberExpression | object member accessor | `this.theMember` |
|
|
74
|
+
| NewExpression | `new` constructor declaration | `const x = new Item()` |
|
|
75
|
+
| ObjectExpression | an object as referenced by its brackets | `{}` |
|
|
76
|
+
| ParenthesizedExpression | an expression wrapped in parens | `const foo = (bar)` |
|
|
77
|
+
| SequenceExpression | deconstructor expressions | `({...(a,b),c})` |
|
|
78
|
+
| TaggedTemplateExpression | backtick expressions | <pre>`${test}`</pre> |
|
|
79
|
+
| ThisExpression | this accessors | `this.someItem` |
|
|
80
|
+
| UnaryExpression | standalone statements | ` return x + 1` (the `x + 1`) |
|
|
81
|
+
| UpdateExpression | increments, decrements | `x++` or `--y` |
|
|
82
|
+
| YieldExpression | yield statements in iterators | `yield x` |
|
|
83
|
+
|
|
84
|
+
## Support
|
|
85
|
+
|
|
86
|
+
Currently only supports VueJS's Single File Components.
|
package/build/file.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { open } from "node:fs/promises";
|
|
11
|
+
import { extname } from "node:path";
|
|
12
|
+
import * as parser from "@babel/parser";
|
|
13
|
+
export function getAst(contents) {
|
|
14
|
+
return parser.parse(contents, {
|
|
15
|
+
sourceType: "module",
|
|
16
|
+
plugins: ["jsx", "typescript"],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export const SCRIPT_OPEN = /^\s*<script(\s[^>]*)?\s*>/;
|
|
20
|
+
export const SCRIPT_CLOSE = /^\s*<\/script>\s*/;
|
|
21
|
+
export function parseVueSFC(lines) {
|
|
22
|
+
const fileContents = [];
|
|
23
|
+
let append = false;
|
|
24
|
+
for (const line of lines.toString().split("\n")) {
|
|
25
|
+
if (SCRIPT_OPEN.test(line)) {
|
|
26
|
+
append = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (SCRIPT_CLOSE.test(line)) {
|
|
30
|
+
append = false;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
if (append) {
|
|
34
|
+
fileContents.push(line);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return fileContents.join("\n");
|
|
38
|
+
}
|
|
39
|
+
const JS_EXTENSIONS = new Set([".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"]);
|
|
40
|
+
export function getAstFromPath(path) {
|
|
41
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
42
|
+
const file = yield open(path);
|
|
43
|
+
const lines = yield file.readFile();
|
|
44
|
+
const ext = extname(path);
|
|
45
|
+
let fileContents;
|
|
46
|
+
if (ext === ".vue") {
|
|
47
|
+
fileContents = parseVueSFC(lines);
|
|
48
|
+
}
|
|
49
|
+
else if (JS_EXTENSIONS.has(ext)) {
|
|
50
|
+
fileContents = lines.toString();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
throw new Error(`Unsupported file extension: "${ext}" in path: ${path}`);
|
|
54
|
+
}
|
|
55
|
+
const ast = getAst(fileContents);
|
|
56
|
+
return { ast, file, source: fileContents };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { isBlockStatement } from "@babel/types";
|
|
2
|
+
export const hasKeyedNode = (node) => {
|
|
3
|
+
switch (node.type) {
|
|
4
|
+
case "ObjectMethod":
|
|
5
|
+
case "ObjectProperty":
|
|
6
|
+
case "ClassMethod":
|
|
7
|
+
case "ClassProperty":
|
|
8
|
+
case "ClassAccessorProperty":
|
|
9
|
+
case "ObjectTypeProperty":
|
|
10
|
+
case "ImportAttribute":
|
|
11
|
+
case "TSDeclareMethod":
|
|
12
|
+
return true;
|
|
13
|
+
case "ClassPrivateProperty":
|
|
14
|
+
case "ClassPrivateMethod":
|
|
15
|
+
return true;
|
|
16
|
+
case "TSPropertySignature":
|
|
17
|
+
case "TSMethodSignature":
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export const hasIdentifier = (node) => {
|
|
22
|
+
switch (node.type) {
|
|
23
|
+
case "PrivateName":
|
|
24
|
+
case "ClassImplements":
|
|
25
|
+
case "DeclareClass":
|
|
26
|
+
case "DeclareFunction":
|
|
27
|
+
case "DeclareInterface":
|
|
28
|
+
case "DeclareModule":
|
|
29
|
+
case "DeclareTypeAlias":
|
|
30
|
+
case "DeclareOpaqueType":
|
|
31
|
+
case "DeclareVariable":
|
|
32
|
+
case "GenericTypeAnnotation":
|
|
33
|
+
case "InterfaceExtends":
|
|
34
|
+
case "InterfaceDeclaration":
|
|
35
|
+
case "ObjectTypeInternalSlot":
|
|
36
|
+
case "OpaqueType":
|
|
37
|
+
case "QualifiedTypeIdentifier":
|
|
38
|
+
case "TypeAlias":
|
|
39
|
+
case "EnumDeclaration":
|
|
40
|
+
case "EnumBooleanMember":
|
|
41
|
+
case "EnumNumberMember":
|
|
42
|
+
case "EnumStringMember":
|
|
43
|
+
case "EnumDefaultedMember":
|
|
44
|
+
case "TSInterfaceDeclaration":
|
|
45
|
+
case "TSTypeAliasDeclaration":
|
|
46
|
+
case "TSEnumDeclaration":
|
|
47
|
+
case "TSEnumMember":
|
|
48
|
+
case "TSModuleDeclaration":
|
|
49
|
+
case "TSImportEqualsDeclaration":
|
|
50
|
+
case "TSNamespaceExportDeclaration":
|
|
51
|
+
case "VariableDeclarator":
|
|
52
|
+
return true;
|
|
53
|
+
default:
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
export const hasBodyBlockStatement = (node) => {
|
|
58
|
+
if ("body" in node) {
|
|
59
|
+
switch (node.type) {
|
|
60
|
+
case "CatchClause":
|
|
61
|
+
case "FunctionDeclaration":
|
|
62
|
+
case "FunctionExpression":
|
|
63
|
+
case "ObjectMethod":
|
|
64
|
+
case "ClassMethod":
|
|
65
|
+
case "ClassPrivateMethod":
|
|
66
|
+
case "DeclareModule":
|
|
67
|
+
case "DoExpression":
|
|
68
|
+
return true;
|
|
69
|
+
case "ArrowFunctionExpression":
|
|
70
|
+
return !!isBlockStatement(node.body);
|
|
71
|
+
default:
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if ("block" in node) {
|
|
76
|
+
return !!isBlockStatement(node.block);
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
};
|
|
80
|
+
export const hasDeclarations = (node) => {
|
|
81
|
+
if ("declarations" in node) {
|
|
82
|
+
switch (node.type) {
|
|
83
|
+
case "VariableDeclaration":
|
|
84
|
+
return true;
|
|
85
|
+
default:
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
export const hasProperties = (node) => {
|
|
91
|
+
if ("properties" in node) {
|
|
92
|
+
switch (node.type) {
|
|
93
|
+
case "ObjectExpression":
|
|
94
|
+
case "ObjectPattern":
|
|
95
|
+
case "ObjectTypeAnnotation":
|
|
96
|
+
case "RecordExpression":
|
|
97
|
+
return true;
|
|
98
|
+
default:
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const arrayifyNode = (node) => {
|
|
104
|
+
if (Array.isArray(node)) {
|
|
105
|
+
return node;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
return [node];
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
export const getNodeBody = (node) => {
|
|
112
|
+
if ("argument" in node) {
|
|
113
|
+
return arrayifyNode(node.argument);
|
|
114
|
+
}
|
|
115
|
+
if ("arguments" in node) {
|
|
116
|
+
return [...node.arguments, ...arrayifyNode(node.callee)];
|
|
117
|
+
}
|
|
118
|
+
if ("block" in node) {
|
|
119
|
+
return arrayifyNode(node.block);
|
|
120
|
+
}
|
|
121
|
+
if ("body" in node) {
|
|
122
|
+
return arrayifyNode(node.body);
|
|
123
|
+
}
|
|
124
|
+
if ("declaration" in node) {
|
|
125
|
+
return arrayifyNode(node.declaration);
|
|
126
|
+
}
|
|
127
|
+
if ("declarations" in node) {
|
|
128
|
+
return node.declarations;
|
|
129
|
+
}
|
|
130
|
+
if ("expression" in node) {
|
|
131
|
+
return arrayifyNode(node.expression);
|
|
132
|
+
}
|
|
133
|
+
if ("init" in node) {
|
|
134
|
+
return arrayifyNode(node.init);
|
|
135
|
+
}
|
|
136
|
+
if ("object" in node) {
|
|
137
|
+
if ("property" in node) {
|
|
138
|
+
return [node.object, ...arrayifyNode(node.property)];
|
|
139
|
+
}
|
|
140
|
+
return arrayifyNode(node.object);
|
|
141
|
+
}
|
|
142
|
+
if ("properties" in node) {
|
|
143
|
+
return node.properties;
|
|
144
|
+
}
|
|
145
|
+
if ("value" in node) {
|
|
146
|
+
return arrayifyNode(node.value);
|
|
147
|
+
}
|
|
148
|
+
return [];
|
|
149
|
+
};
|
package/build/main.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
12
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
13
|
+
var m = o[Symbol.asyncIterator], i;
|
|
14
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
15
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
16
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
17
|
+
};
|
|
18
|
+
import yargs from "yargs/yargs";
|
|
19
|
+
import { walkRepoFiles } from "./walk.js";
|
|
20
|
+
import { getAstFromPath } from "./file.js";
|
|
21
|
+
import { runQuery, validateSelector } from "./search.js";
|
|
22
|
+
import { formatMatches } from "./output.js";
|
|
23
|
+
export function searchRepo(selector, dir) {
|
|
24
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25
|
+
var _a, e_1, _b, _c;
|
|
26
|
+
validateSelector(selector); // throws early on invalid selector syntax
|
|
27
|
+
const results = [];
|
|
28
|
+
try {
|
|
29
|
+
for (var _d = true, _e = __asyncValues(walkRepoFiles(dir)), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
|
|
30
|
+
_c = _f.value;
|
|
31
|
+
_d = false;
|
|
32
|
+
const filePath = _c;
|
|
33
|
+
let file;
|
|
34
|
+
try {
|
|
35
|
+
const result = yield getAstFromPath(filePath);
|
|
36
|
+
file = result.file;
|
|
37
|
+
const matches = runQuery(selector, result.ast, result.source, filePath);
|
|
38
|
+
results.push(...matches);
|
|
39
|
+
}
|
|
40
|
+
catch (_g) {
|
|
41
|
+
// skip unparseable files
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
yield (file === null || file === void 0 ? void 0 : file.close());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
49
|
+
finally {
|
|
50
|
+
try {
|
|
51
|
+
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
|
|
52
|
+
}
|
|
53
|
+
finally { if (e_1) throw e_1.error; }
|
|
54
|
+
}
|
|
55
|
+
return results;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const y = yargs(process.argv.slice(2))
|
|
59
|
+
.scriptName("ast-search")
|
|
60
|
+
.usage("$0 <query> [--dir <path>] [--format <fmt>]")
|
|
61
|
+
.command("$0 <query>", "Search a repo for AST patterns using CSS selector syntax", (yargs) => yargs
|
|
62
|
+
.positional("query", {
|
|
63
|
+
type: "string",
|
|
64
|
+
describe: "esquery CSS selector string",
|
|
65
|
+
demandOption: true,
|
|
66
|
+
})
|
|
67
|
+
.option("dir", {
|
|
68
|
+
alias: "d",
|
|
69
|
+
type: "string",
|
|
70
|
+
describe: "root directory to search",
|
|
71
|
+
default: process.cwd(),
|
|
72
|
+
})
|
|
73
|
+
.option("format", {
|
|
74
|
+
alias: "f",
|
|
75
|
+
type: "string",
|
|
76
|
+
describe: "output format: text (default), json, files",
|
|
77
|
+
default: "text",
|
|
78
|
+
choices: ["text", "json", "files"],
|
|
79
|
+
}), (argv) => __awaiter(void 0, void 0, void 0, function* () {
|
|
80
|
+
var _a;
|
|
81
|
+
const { query, dir, format } = argv;
|
|
82
|
+
try {
|
|
83
|
+
const matches = yield searchRepo(query, dir);
|
|
84
|
+
const isTTY = (_a = process.stdout.isTTY) !== null && _a !== void 0 ? _a : false;
|
|
85
|
+
for (const line of formatMatches(matches, isTTY, format)) {
|
|
86
|
+
console.log(line);
|
|
87
|
+
}
|
|
88
|
+
process.exit(matches.length > 0 ? 0 : 1);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
92
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
93
|
+
process.exit(2);
|
|
94
|
+
}
|
|
95
|
+
}))
|
|
96
|
+
.help();
|
|
97
|
+
if (process.env.NODE_ENV !== "test") {
|
|
98
|
+
y.parse();
|
|
99
|
+
}
|
package/build/output.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const BOLD = "\x1b[1m";
|
|
2
|
+
const CYAN = "\x1b[36m";
|
|
3
|
+
const RESET = "\x1b[0m";
|
|
4
|
+
export function formatMatches(matches, isTTY, format = "text") {
|
|
5
|
+
if (format === "json") {
|
|
6
|
+
return [JSON.stringify(matches, null, 2)];
|
|
7
|
+
}
|
|
8
|
+
if (format === "files") {
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
return matches.map((m) => m.file).filter((f) => !seen.has(f) && !!seen.add(f));
|
|
11
|
+
}
|
|
12
|
+
return matches.map((m) => formatMatch(m, isTTY));
|
|
13
|
+
}
|
|
14
|
+
function formatMatch(match, isTTY) {
|
|
15
|
+
const loc = `${match.file}:${match.line}:${match.col}`;
|
|
16
|
+
if (!match.source)
|
|
17
|
+
return loc;
|
|
18
|
+
const src = isTTY ? `${BOLD}${CYAN}${match.source}${RESET}` : match.source;
|
|
19
|
+
return `${loc}: ${src}`;
|
|
20
|
+
}
|
package/build/search.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import esquery from "esquery";
|
|
2
|
+
import { VISITOR_KEYS } from "@babel/types";
|
|
3
|
+
export const SHORTHANDS = {
|
|
4
|
+
this: "ThisExpression",
|
|
5
|
+
await: "AwaitExpression",
|
|
6
|
+
yield: "YieldExpression",
|
|
7
|
+
new: "NewExpression",
|
|
8
|
+
call: "CallExpression",
|
|
9
|
+
arrow: "ArrowFunctionExpression",
|
|
10
|
+
fn: "FunctionExpression",
|
|
11
|
+
member: "MemberExpression",
|
|
12
|
+
ternary: "ConditionalExpression",
|
|
13
|
+
template: "TemplateLiteral",
|
|
14
|
+
tagged: "TaggedTemplateExpression",
|
|
15
|
+
assign: "AssignmentExpression",
|
|
16
|
+
binary: "BinaryExpression",
|
|
17
|
+
logical: "LogicalExpression",
|
|
18
|
+
spread: "SpreadElement",
|
|
19
|
+
};
|
|
20
|
+
// Expand shorthands to full Babel type names, but not inside quoted attribute values.
|
|
21
|
+
export function expandShorthands(selector) {
|
|
22
|
+
const keys = Object.keys(SHORTHANDS);
|
|
23
|
+
const pattern = new RegExp(`\\b(${keys.join("|")})\\b`, "g");
|
|
24
|
+
const parts = [];
|
|
25
|
+
let i = 0;
|
|
26
|
+
while (i < selector.length) {
|
|
27
|
+
const ch = selector[i];
|
|
28
|
+
if (ch === '"' || ch === "'") {
|
|
29
|
+
// Preserve quoted string as-is
|
|
30
|
+
let j = i + 1;
|
|
31
|
+
while (j < selector.length && selector[j] !== ch)
|
|
32
|
+
j++;
|
|
33
|
+
parts.push(selector.slice(i, j + 1));
|
|
34
|
+
i = j + 1;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const nextQuote = selector.slice(i).search(/['"]/);
|
|
38
|
+
const segment = nextQuote === -1 ? selector.slice(i) : selector.slice(i, i + nextQuote);
|
|
39
|
+
parts.push(segment.replace(pattern, (m) => { var _a; return (_a = SHORTHANDS[m]) !== null && _a !== void 0 ? _a : m; }));
|
|
40
|
+
i = nextQuote === -1 ? selector.length : i + nextQuote;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return parts.join("");
|
|
44
|
+
}
|
|
45
|
+
function extractFirstLine(source, node) {
|
|
46
|
+
var _a;
|
|
47
|
+
if (!source || node.start == null)
|
|
48
|
+
return "";
|
|
49
|
+
const text = source.slice(node.start, (_a = node.end) !== null && _a !== void 0 ? _a : node.start);
|
|
50
|
+
return text.split("\n")[0].trim();
|
|
51
|
+
}
|
|
52
|
+
export function validateSelector(selector) {
|
|
53
|
+
esquery.parse(expandShorthands(selector));
|
|
54
|
+
}
|
|
55
|
+
export function runQuery(selector, ast, source = "", filename = "") {
|
|
56
|
+
const expanded = expandShorthands(selector);
|
|
57
|
+
// Cast to any: esquery expects estree.Node but Babel AST is structurally
|
|
58
|
+
// compatible; VISITOR_KEYS ensures correct traversal of Babel-specific nodes.
|
|
59
|
+
const nodes = esquery.query(ast, expanded, {
|
|
60
|
+
visitorKeys: VISITOR_KEYS,
|
|
61
|
+
});
|
|
62
|
+
return nodes.map((node) => {
|
|
63
|
+
var _a, _b, _c, _d;
|
|
64
|
+
return ({
|
|
65
|
+
file: filename,
|
|
66
|
+
line: (_b = (_a = node.loc) === null || _a === void 0 ? void 0 : _a.start.line) !== null && _b !== void 0 ? _b : 0,
|
|
67
|
+
col: (_d = (_c = node.loc) === null || _c === void 0 ? void 0 : _c.start.column) !== null && _d !== void 0 ? _d : 0,
|
|
68
|
+
source: extractFirstLine(source, node),
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
package/build/walk.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
|
|
2
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
3
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
4
|
+
var m = o[Symbol.asyncIterator], i;
|
|
5
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
6
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
7
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
8
|
+
};
|
|
9
|
+
var __asyncDelegator = (this && this.__asyncDelegator) || function (o) {
|
|
10
|
+
var i, p;
|
|
11
|
+
return i = {}, verb("next"), verb("throw", function (e) { throw e; }), verb("return"), i[Symbol.iterator] = function () { return this; }, i;
|
|
12
|
+
function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }
|
|
13
|
+
};
|
|
14
|
+
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
|
|
15
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
16
|
+
var g = generator.apply(thisArg, _arguments || []), i, q = [];
|
|
17
|
+
return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
|
|
18
|
+
function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
|
|
19
|
+
function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
|
|
20
|
+
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
|
|
21
|
+
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
|
|
22
|
+
function fulfill(value) { resume("next", value); }
|
|
23
|
+
function reject(value) { resume("throw", value); }
|
|
24
|
+
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
|
|
25
|
+
};
|
|
26
|
+
import { readdir } from "node:fs/promises";
|
|
27
|
+
import { extname, join } from "node:path";
|
|
28
|
+
const SUPPORTED_EXTENSIONS = new Set([
|
|
29
|
+
".js",
|
|
30
|
+
".ts",
|
|
31
|
+
".jsx",
|
|
32
|
+
".tsx",
|
|
33
|
+
".mjs",
|
|
34
|
+
".cjs",
|
|
35
|
+
".vue",
|
|
36
|
+
]);
|
|
37
|
+
export function walkRepoFiles(dir) {
|
|
38
|
+
return __asyncGenerator(this, arguments, function* walkRepoFiles_1() {
|
|
39
|
+
const entries = yield __await(readdir(dir, { withFileTypes: true }));
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (entry.name.startsWith("."))
|
|
42
|
+
continue;
|
|
43
|
+
if (entry.name === "node_modules")
|
|
44
|
+
continue;
|
|
45
|
+
const fullPath = join(dir, entry.name);
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
yield __await(yield* __asyncDelegator(__asyncValues(walkRepoFiles(fullPath))));
|
|
48
|
+
}
|
|
49
|
+
else if (entry.isFile() && SUPPORTED_EXTENSIONS.has(extname(entry.name))) {
|
|
50
|
+
yield yield __await(fullPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ast-search",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "main.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "npx tsc --watch",
|
|
8
|
+
"debug:vue": "npx tsc && node ./build/main.js 'setup > this' --dir src/__tests__/dummyFiles",
|
|
9
|
+
"debug:react": "npx tsc && node ./build/main.js 'useState' --dir src/__tests__/dummyFiles",
|
|
10
|
+
"debug:empty": "npx tsc && node ./build/main.js 'ValidationsTab > this' --dir src/__tests__/dummyFiles",
|
|
11
|
+
"debug:basics": "npx tsc && node ./build/main.js 'arrowFunction' --dir src/__tests__/dummyFiles",
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"build": "npx tsc && cp build/main.js ast-search",
|
|
14
|
+
"prepublishOnly": "npx tsc"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"provenance": true
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"author": "shiplet",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/shiplet/ast-search.git"
|
|
26
|
+
},
|
|
27
|
+
"packageManager": "pnpm@10.33.0",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@babel/parser": "^7.24.4",
|
|
30
|
+
"@babel/types": "^7.24.0",
|
|
31
|
+
"esquery": "^1.7.0",
|
|
32
|
+
"yargs": "^17.7.2"
|
|
33
|
+
},
|
|
34
|
+
"bin": {
|
|
35
|
+
"ast-search": "./build/main.js"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"build/*.js",
|
|
39
|
+
"build/helpers/*.js",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"pkg": {
|
|
43
|
+
"targets": [
|
|
44
|
+
"node18",
|
|
45
|
+
"macos-x64"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@babel/preset-typescript": "^7.24.1",
|
|
50
|
+
"@jest/globals": "^29.7.0",
|
|
51
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
52
|
+
"@semantic-release/git": "^10.0.1",
|
|
53
|
+
"@semantic-release/github": "^10.0.0",
|
|
54
|
+
"@semantic-release/npm": "^13.1.3",
|
|
55
|
+
"@types/esquery": "^1.5.4",
|
|
56
|
+
"@types/jest": "^29.5.12",
|
|
57
|
+
"@types/node": "^20.12.7",
|
|
58
|
+
"jest": "^29.7.0",
|
|
59
|
+
"memfs": "^4.8.1",
|
|
60
|
+
"semantic-release": "^25.0.3",
|
|
61
|
+
"ts-jest": "^29.1.2",
|
|
62
|
+
"typescript": "^5.4.5"
|
|
63
|
+
}
|
|
64
|
+
}
|