@rettangoli/check 0.0.1
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 +295 -0
- package/ROADMAP.md +175 -0
- package/package.json +46 -0
- package/src/cli/bin.js +325 -0
- package/src/cli/check.js +232 -0
- package/src/cli/index.js +1 -0
- package/src/core/analyze.js +227 -0
- package/src/core/discovery.js +83 -0
- package/src/core/exportedFunctions.js +235 -0
- package/src/core/model.js +898 -0
- package/src/core/parsers.js +2726 -0
- package/src/core/registry.js +779 -0
- package/src/core/schema.js +161 -0
- package/src/core/scopeGraph.js +1400 -0
- package/src/core/semantic.js +329 -0
- package/src/diagnostics/autofix.js +191 -0
- package/src/diagnostics/catalog.js +89 -0
- package/src/index.js +2 -0
- package/src/reporters/index.js +13 -0
- package/src/reporters/json.js +42 -0
- package/src/reporters/sarif.js +213 -0
- package/src/reporters/text.js +145 -0
- package/src/rules/compatibility.js +318 -0
- package/src/rules/constants.js +22 -0
- package/src/rules/crossFileSymbols.js +108 -0
- package/src/rules/expression.js +338 -0
- package/src/rules/feParity.js +65 -0
- package/src/rules/index.js +39 -0
- package/src/rules/jempl.js +80 -0
- package/src/rules/lifecycle.js +4 -0
- package/src/rules/listenerConfig.js +556 -0
- package/src/rules/listenerSymbols.js +49 -0
- package/src/rules/methods.js +117 -0
- package/src/rules/refs.js +20 -0
- package/src/rules/schema.js +118 -0
- package/src/rules/shared.js +20 -0
- package/src/rules/yahtmlAttrs.js +238 -0
- package/src/semantic/engine.js +778 -0
- package/src/semantic/index.js +9 -0
- package/src/types/lattice.js +281 -0
- package/src/utils/case.js +9 -0
- package/src/utils/fs.js +30 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { buildComponentModel, buildProjectModel } from "./model.js";
|
|
3
|
+
import { discoverComponentEntries, groupEntriesByComponent } from "./discovery.js";
|
|
4
|
+
import { buildMergedRegistry } from "./registry.js";
|
|
5
|
+
import { runRules } from "../rules/index.js";
|
|
6
|
+
import { runSemanticEngine } from "../semantic/engine.js";
|
|
7
|
+
import { getDiagnosticCatalogEntry } from "../diagnostics/catalog.js";
|
|
8
|
+
|
|
9
|
+
const summarizeDiagnostics = (diagnostics = []) => {
|
|
10
|
+
const bySeverity = { error: 0, warn: 0 };
|
|
11
|
+
const byCodeMap = new Map();
|
|
12
|
+
|
|
13
|
+
diagnostics.forEach((diag) => {
|
|
14
|
+
const severity = diag.severity === "warn" ? "warn" : "error";
|
|
15
|
+
bySeverity[severity] += 1;
|
|
16
|
+
byCodeMap.set(diag.code, (byCodeMap.get(diag.code) || 0) + 1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const byCode = [...byCodeMap.entries()]
|
|
20
|
+
.map(([code, count]) => ({ code, count }))
|
|
21
|
+
.sort((a, b) => a.code.localeCompare(b.code));
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
total: diagnostics.length,
|
|
25
|
+
bySeverity,
|
|
26
|
+
byCode,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const categorizeDiagnosticCode = (code = "") => {
|
|
31
|
+
if (code.includes("YAHTML")) return "template";
|
|
32
|
+
if (code.includes("JEMPL")) return "template";
|
|
33
|
+
if (code.includes("SYMBOL")) return "symbols";
|
|
34
|
+
if (code.includes("SCHEMA")) return "schema";
|
|
35
|
+
if (code.includes("LIFECYCLE")) return "lifecycle";
|
|
36
|
+
if (code.includes("EXPR")) return "expression";
|
|
37
|
+
if (code.includes("COMPAT")) return "compatibility";
|
|
38
|
+
if (code.includes("CONTRACT")) return "contracts";
|
|
39
|
+
return "general";
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const normalizeRelatedLocations = (related = []) => {
|
|
43
|
+
if (!Array.isArray(related) || related.length === 0) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const normalized = related
|
|
48
|
+
.filter((entry) => entry && typeof entry === "object")
|
|
49
|
+
.map((entry) => ({
|
|
50
|
+
message: typeof entry.message === "string" && entry.message.trim()
|
|
51
|
+
? entry.message.trim()
|
|
52
|
+
: undefined,
|
|
53
|
+
filePath: typeof entry.filePath === "string" && entry.filePath
|
|
54
|
+
? entry.filePath
|
|
55
|
+
: "unknown",
|
|
56
|
+
line: Number.isInteger(entry.line) ? entry.line : undefined,
|
|
57
|
+
column: Number.isInteger(entry.column) ? entry.column : undefined,
|
|
58
|
+
endLine: Number.isInteger(entry.endLine) ? entry.endLine : undefined,
|
|
59
|
+
endColumn: Number.isInteger(entry.endColumn) ? entry.endColumn : undefined,
|
|
60
|
+
}))
|
|
61
|
+
.sort((left, right) => (
|
|
62
|
+
left.filePath.localeCompare(right.filePath)
|
|
63
|
+
|| (left.line || 0) - (right.line || 0)
|
|
64
|
+
|| (left.column || 0) - (right.column || 0)
|
|
65
|
+
|| (left.message || "").localeCompare(right.message || "")
|
|
66
|
+
));
|
|
67
|
+
|
|
68
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const normalizeFix = (fix = {}, diagnostic = {}) => {
|
|
72
|
+
if (!fix || typeof fix !== "object") {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const normalized = {
|
|
77
|
+
kind: typeof fix.kind === "string" ? fix.kind : undefined,
|
|
78
|
+
description: typeof fix.description === "string" && fix.description.trim()
|
|
79
|
+
? fix.description.trim()
|
|
80
|
+
: undefined,
|
|
81
|
+
safe: fix.safe !== false,
|
|
82
|
+
confidence: Number.isFinite(fix.confidence) ? Number(fix.confidence) : undefined,
|
|
83
|
+
filePath: typeof fix.filePath === "string" && fix.filePath
|
|
84
|
+
? fix.filePath
|
|
85
|
+
: (diagnostic.filePath || "unknown"),
|
|
86
|
+
line: Number.isInteger(fix.line) ? fix.line : undefined,
|
|
87
|
+
column: Number.isInteger(fix.column) ? fix.column : undefined,
|
|
88
|
+
endLine: Number.isInteger(fix.endLine) ? fix.endLine : undefined,
|
|
89
|
+
endColumn: Number.isInteger(fix.endColumn) ? fix.endColumn : undefined,
|
|
90
|
+
pattern: typeof fix.pattern === "string" && fix.pattern ? fix.pattern : undefined,
|
|
91
|
+
replacement: typeof fix.replacement === "string" ? fix.replacement : undefined,
|
|
92
|
+
flags: typeof fix.flags === "string" && fix.flags ? fix.flags : undefined,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (!normalized.kind) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return normalized;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const normalizeTrace = (trace = []) => {
|
|
103
|
+
if (!Array.isArray(trace) || trace.length === 0) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const normalized = trace
|
|
108
|
+
.map((entry) => (entry === null || entry === undefined ? "" : String(entry).trim()))
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.sort((left, right) => left.localeCompare(right));
|
|
111
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const normalizeDiagnostics = (diagnostics = []) => {
|
|
115
|
+
return diagnostics
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
.map((diag) => {
|
|
118
|
+
const code = diag.code || "RTGL-CHECK-UNKNOWN";
|
|
119
|
+
const catalog = getDiagnosticCatalogEntry(code);
|
|
120
|
+
const related = normalizeRelatedLocations(diag.related);
|
|
121
|
+
const trace = normalizeTrace(diag.trace || related?.map((entry) => entry.message).filter(Boolean));
|
|
122
|
+
return {
|
|
123
|
+
code,
|
|
124
|
+
category: categorizeDiagnosticCode(code),
|
|
125
|
+
severity: diag.severity === "warn" ? "warn" : "error",
|
|
126
|
+
message: String(diag.message || "Unknown diagnostic"),
|
|
127
|
+
filePath: diag.filePath || "unknown",
|
|
128
|
+
line: Number.isInteger(diag.line) ? diag.line : undefined,
|
|
129
|
+
column: Number.isInteger(diag.column) ? diag.column : undefined,
|
|
130
|
+
endLine: Number.isInteger(diag.endLine) ? diag.endLine : undefined,
|
|
131
|
+
endColumn: Number.isInteger(diag.endColumn) ? diag.endColumn : undefined,
|
|
132
|
+
title: catalog.title,
|
|
133
|
+
family: catalog.family,
|
|
134
|
+
docsPath: catalog.docsPath,
|
|
135
|
+
namespaceValid: catalog.namespaceValid,
|
|
136
|
+
tags: catalog.tags,
|
|
137
|
+
related,
|
|
138
|
+
trace,
|
|
139
|
+
fix: normalizeFix(diag.fix, diag),
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const analyzeProject = async ({
|
|
145
|
+
cwd = process.cwd(),
|
|
146
|
+
dirs = [],
|
|
147
|
+
workspaceRoot = cwd,
|
|
148
|
+
includeYahtml = true,
|
|
149
|
+
includeExpression = false,
|
|
150
|
+
includeSemantic = false,
|
|
151
|
+
incrementalState,
|
|
152
|
+
} = {}) => {
|
|
153
|
+
const discovery = discoverComponentEntries({ cwd, dirs });
|
|
154
|
+
const componentGroups = groupEntriesByComponent(discovery.entries);
|
|
155
|
+
const models = (() => {
|
|
156
|
+
if (!incrementalState || !(incrementalState.componentCache instanceof Map)) {
|
|
157
|
+
return buildProjectModel(componentGroups);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const buildFingerprint = (files = {}) => {
|
|
161
|
+
return Object.entries(files)
|
|
162
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
163
|
+
.map(([fileType, filePath]) => {
|
|
164
|
+
try {
|
|
165
|
+
const stat = statSync(filePath);
|
|
166
|
+
return `${fileType}:${filePath}:${stat.mtimeMs}:${stat.size}`;
|
|
167
|
+
} catch {
|
|
168
|
+
return `${fileType}:${filePath}:missing`;
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
.join("|");
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const nextCache = new Map();
|
|
175
|
+
const nextModels = componentGroups.map((componentGroup) => {
|
|
176
|
+
const fingerprint = buildFingerprint(componentGroup.files);
|
|
177
|
+
const cached = incrementalState.componentCache.get(componentGroup.componentKey);
|
|
178
|
+
if (cached && cached.fingerprint === fingerprint) {
|
|
179
|
+
nextCache.set(componentGroup.componentKey, cached);
|
|
180
|
+
return cached.model;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const model = buildComponentModel(componentGroup);
|
|
184
|
+
nextCache.set(componentGroup.componentKey, {
|
|
185
|
+
fingerprint,
|
|
186
|
+
model,
|
|
187
|
+
});
|
|
188
|
+
return model;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
incrementalState.componentCache = nextCache;
|
|
192
|
+
return nextModels;
|
|
193
|
+
})();
|
|
194
|
+
|
|
195
|
+
const modelDiagnostics = normalizeDiagnostics(models.flatMap((model) => model.diagnostics || []));
|
|
196
|
+
const registry = await buildMergedRegistry({ models, workspaceRoot });
|
|
197
|
+
const ruleDiagnostics = normalizeDiagnostics(runRules({
|
|
198
|
+
models,
|
|
199
|
+
registry,
|
|
200
|
+
includeYahtml,
|
|
201
|
+
includeExpression,
|
|
202
|
+
}));
|
|
203
|
+
const semanticResult = includeSemantic
|
|
204
|
+
? runSemanticEngine({ models, registry })
|
|
205
|
+
: null;
|
|
206
|
+
const semanticDiagnostics = includeSemantic
|
|
207
|
+
? normalizeDiagnostics(semanticResult?.diagnostics || [])
|
|
208
|
+
: [];
|
|
209
|
+
const diagnostics = [...modelDiagnostics, ...ruleDiagnostics, ...semanticDiagnostics];
|
|
210
|
+
const summary = summarizeDiagnostics(diagnostics);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
ok: summary.bySeverity.error === 0,
|
|
214
|
+
cwd,
|
|
215
|
+
dirs: discovery.resolvedDirs,
|
|
216
|
+
componentCount: models.length,
|
|
217
|
+
diagnostics,
|
|
218
|
+
summary,
|
|
219
|
+
registryTagCount: registry.size,
|
|
220
|
+
semantic: semanticResult
|
|
221
|
+
? {
|
|
222
|
+
diagnosticsCount: semanticDiagnostics.length,
|
|
223
|
+
invariants: semanticResult.invariants,
|
|
224
|
+
}
|
|
225
|
+
: undefined,
|
|
226
|
+
};
|
|
227
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { walkFiles, isDirectoryPath } from "../utils/fs.js";
|
|
3
|
+
|
|
4
|
+
const FILE_SUFFIX_MAP = [
|
|
5
|
+
[".view.yaml", "view"],
|
|
6
|
+
[".schema.yaml", "schema"],
|
|
7
|
+
[".store.js", "store"],
|
|
8
|
+
[".handlers.js", "handlers"],
|
|
9
|
+
[".methods.js", "methods"],
|
|
10
|
+
[".constants.yaml", "constants"],
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export const SUPPORTED_FILE_SUFFIXES = FILE_SUFFIX_MAP.map(([suffix]) => suffix);
|
|
14
|
+
|
|
15
|
+
const detectFileType = (filePath) => {
|
|
16
|
+
for (const [suffix, fileType] of FILE_SUFFIX_MAP) {
|
|
17
|
+
if (filePath.endsWith(suffix)) {
|
|
18
|
+
return { suffix, fileType };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const resolveDirs = ({ cwd = process.cwd(), dirs = [] }) => {
|
|
25
|
+
return dirs
|
|
26
|
+
.map((dirPath) => path.resolve(cwd, dirPath))
|
|
27
|
+
.filter((dirPath) => isDirectoryPath(dirPath));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const discoverComponentEntries = ({ cwd = process.cwd(), dirs = [] }) => {
|
|
31
|
+
const resolvedDirs = resolveDirs({ cwd, dirs });
|
|
32
|
+
const allFiles = walkFiles(resolvedDirs);
|
|
33
|
+
const supportedEntries = [];
|
|
34
|
+
|
|
35
|
+
allFiles.forEach((filePath) => {
|
|
36
|
+
const fileTypeInfo = detectFileType(filePath);
|
|
37
|
+
if (!fileTypeInfo) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const normalizedPath = path.resolve(filePath);
|
|
42
|
+
const fileName = path.basename(normalizedPath);
|
|
43
|
+
const componentDir = path.basename(path.dirname(normalizedPath));
|
|
44
|
+
const categoryDir = path.basename(path.dirname(path.dirname(normalizedPath)));
|
|
45
|
+
const componentNameFromFile = fileName.slice(0, -fileTypeInfo.suffix.length);
|
|
46
|
+
|
|
47
|
+
supportedEntries.push({
|
|
48
|
+
filePath: normalizedPath,
|
|
49
|
+
fileName,
|
|
50
|
+
fileType: fileTypeInfo.fileType,
|
|
51
|
+
category: categoryDir,
|
|
52
|
+
component: componentDir,
|
|
53
|
+
componentNameFromFile,
|
|
54
|
+
componentKey: `${categoryDir}/${componentDir}`,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
resolvedDirs,
|
|
60
|
+
allFiles,
|
|
61
|
+
entries: supportedEntries,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const groupEntriesByComponent = (entries = []) => {
|
|
66
|
+
const byComponent = new Map();
|
|
67
|
+
|
|
68
|
+
entries.forEach((entry) => {
|
|
69
|
+
const existing = byComponent.get(entry.componentKey) || {
|
|
70
|
+
componentKey: entry.componentKey,
|
|
71
|
+
category: entry.category,
|
|
72
|
+
component: entry.component,
|
|
73
|
+
files: {},
|
|
74
|
+
entries: [],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
existing.files[entry.fileType] = entry.filePath;
|
|
78
|
+
existing.entries.push(entry);
|
|
79
|
+
byComponent.set(entry.componentKey, existing);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return [...byComponent.values()];
|
|
83
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { parseSync } from "oxc-parser";
|
|
2
|
+
|
|
3
|
+
const createLineOffsets = (source = "") => {
|
|
4
|
+
const offsets = [0];
|
|
5
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
6
|
+
if (source[index] === "\n") {
|
|
7
|
+
offsets.push(index + 1);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return offsets;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const offsetToLine = ({ lineOffsets = [], offset = 0 }) => {
|
|
14
|
+
let low = 0;
|
|
15
|
+
let high = lineOffsets.length - 1;
|
|
16
|
+
while (low <= high) {
|
|
17
|
+
const mid = Math.floor((low + high) / 2);
|
|
18
|
+
const start = lineOffsets[mid];
|
|
19
|
+
const next = lineOffsets[mid + 1] ?? Number.POSITIVE_INFINITY;
|
|
20
|
+
if (offset >= start && offset < next) {
|
|
21
|
+
return mid + 1;
|
|
22
|
+
}
|
|
23
|
+
if (offset < start) {
|
|
24
|
+
high = mid - 1;
|
|
25
|
+
} else {
|
|
26
|
+
low = mid + 1;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const getPropertyKeyName = (keyNode) => {
|
|
33
|
+
if (!keyNode || typeof keyNode !== "object") {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (keyNode.type === "Identifier" && typeof keyNode.name === "string" && keyNode.name) {
|
|
37
|
+
return keyNode.name;
|
|
38
|
+
}
|
|
39
|
+
if (keyNode.type === "StringLiteral" && typeof keyNode.value === "string" && keyNode.value) {
|
|
40
|
+
return keyNode.value;
|
|
41
|
+
}
|
|
42
|
+
if (keyNode.type === "Literal" && typeof keyNode.value === "string" && keyNode.value) {
|
|
43
|
+
return keyNode.value;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const describeObjectPattern = (pattern = {}) => {
|
|
49
|
+
if (!pattern || pattern.type !== "ObjectPattern" || !Array.isArray(pattern.properties)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const keys = new Set();
|
|
54
|
+
let hasRest = false;
|
|
55
|
+
|
|
56
|
+
pattern.properties.forEach((property) => {
|
|
57
|
+
if (!property || typeof property !== "object") {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (property.type === "RestElement") {
|
|
62
|
+
hasRest = true;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (property.type !== "Property" || property.computed) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const keyName = getPropertyKeyName(property.key);
|
|
71
|
+
if (keyName) {
|
|
72
|
+
keys.add(keyName);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
kind: "object",
|
|
78
|
+
objectKeys: [...keys].sort((left, right) => left.localeCompare(right)),
|
|
79
|
+
hasRest,
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const describeParam = (param = {}) => {
|
|
84
|
+
if (!param || typeof param !== "object") {
|
|
85
|
+
return {
|
|
86
|
+
kind: "unknown",
|
|
87
|
+
name: null,
|
|
88
|
+
objectKeys: [],
|
|
89
|
+
hasRest: false,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (param.type === "Identifier") {
|
|
94
|
+
return {
|
|
95
|
+
kind: "identifier",
|
|
96
|
+
name: param.name || null,
|
|
97
|
+
objectKeys: [],
|
|
98
|
+
hasRest: false,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (param.type === "AssignmentPattern") {
|
|
103
|
+
const left = describeParam(param.left);
|
|
104
|
+
return {
|
|
105
|
+
...left,
|
|
106
|
+
kind: left.kind === "unknown" ? "assignment" : left.kind,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const objectPattern = describeObjectPattern(param);
|
|
111
|
+
if (objectPattern) {
|
|
112
|
+
return {
|
|
113
|
+
kind: "object",
|
|
114
|
+
name: null,
|
|
115
|
+
objectKeys: objectPattern.objectKeys,
|
|
116
|
+
hasRest: objectPattern.hasRest,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
kind: "pattern",
|
|
122
|
+
name: null,
|
|
123
|
+
objectKeys: [],
|
|
124
|
+
hasRest: false,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const toFunctionMeta = ({ name, node, lineOffsets = [] }) => {
|
|
129
|
+
if (!name || !node || typeof node !== "object") {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const params = Array.isArray(node.params) ? node.params : [];
|
|
134
|
+
const firstParam = params.length > 0
|
|
135
|
+
? describeParam(params[0])
|
|
136
|
+
: {
|
|
137
|
+
kind: "none",
|
|
138
|
+
name: null,
|
|
139
|
+
objectKeys: [],
|
|
140
|
+
hasRest: false,
|
|
141
|
+
};
|
|
142
|
+
const secondParam = params.length > 1
|
|
143
|
+
? describeParam(params[1])
|
|
144
|
+
: {
|
|
145
|
+
kind: "none",
|
|
146
|
+
name: null,
|
|
147
|
+
objectKeys: [],
|
|
148
|
+
hasRest: false,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
name,
|
|
153
|
+
async: Boolean(node.async),
|
|
154
|
+
paramCount: params.length,
|
|
155
|
+
firstParamName: firstParam.name,
|
|
156
|
+
secondParamName: secondParam.name,
|
|
157
|
+
firstParam,
|
|
158
|
+
secondParam,
|
|
159
|
+
line: offsetToLine({
|
|
160
|
+
lineOffsets,
|
|
161
|
+
offset: Number(node.start) || 0,
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const parseNamedExportedFunctions = ({
|
|
167
|
+
sourceText = "",
|
|
168
|
+
filePath = "unknown.js",
|
|
169
|
+
} = {}) => {
|
|
170
|
+
const functions = new Map();
|
|
171
|
+
let parsed = null;
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
parsed = parseSync(filePath, sourceText, { sourceType: "unambiguous" });
|
|
175
|
+
} catch {
|
|
176
|
+
return functions;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!parsed?.program?.body || !Array.isArray(parsed.program.body)) {
|
|
180
|
+
return functions;
|
|
181
|
+
}
|
|
182
|
+
if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
|
|
183
|
+
return functions;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const lineOffsets = createLineOffsets(sourceText);
|
|
187
|
+
|
|
188
|
+
parsed.program.body.forEach((statement) => {
|
|
189
|
+
if (!statement || statement.type !== "ExportNamedDeclaration") {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const declaration = statement.declaration;
|
|
194
|
+
if (!declaration) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (declaration.type === "FunctionDeclaration" && declaration.id?.name) {
|
|
199
|
+
const meta = toFunctionMeta({
|
|
200
|
+
name: declaration.id.name,
|
|
201
|
+
node: declaration,
|
|
202
|
+
lineOffsets,
|
|
203
|
+
});
|
|
204
|
+
if (meta) {
|
|
205
|
+
functions.set(meta.name, meta);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (declaration.type !== "VariableDeclaration" || !Array.isArray(declaration.declarations)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
declaration.declarations.forEach((declarator) => {
|
|
215
|
+
const exportName = declarator?.id?.type === "Identifier" ? declarator.id.name : null;
|
|
216
|
+
if (!exportName) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const init = declarator.init;
|
|
220
|
+
if (!init || (init.type !== "ArrowFunctionExpression" && init.type !== "FunctionExpression")) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const meta = toFunctionMeta({
|
|
224
|
+
name: exportName,
|
|
225
|
+
node: init,
|
|
226
|
+
lineOffsets,
|
|
227
|
+
});
|
|
228
|
+
if (meta) {
|
|
229
|
+
functions.set(meta.name, meta);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return functions;
|
|
235
|
+
};
|