resultar-check 1.0.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/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/cli.cjs +32 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +33 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +95 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/lint-D-D2dmLi.cjs +1030 -0
- package/dist/lint-D-D2dmLi.cjs.map +1 -0
- package/dist/lint-FMWf8UEv.js +1001 -0
- package/dist/lint-FMWf8UEv.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
let node_child_process = require("node:child_process");
|
|
2
|
+
let node_fs = require("node:fs");
|
|
3
|
+
let node_module = require("node:module");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
//#region src/result-usage-core.ts
|
|
6
|
+
const resultTypeNames = new Set([
|
|
7
|
+
"DisposableResult",
|
|
8
|
+
"DisposableResultAsync",
|
|
9
|
+
"ErrResult",
|
|
10
|
+
"OkResult",
|
|
11
|
+
"Result",
|
|
12
|
+
"ResultAsync",
|
|
13
|
+
"StrictResult",
|
|
14
|
+
"StrictResultAsync"
|
|
15
|
+
]);
|
|
16
|
+
const consumerMethods = new Set([
|
|
17
|
+
"_unsafeUnwrap",
|
|
18
|
+
"_unsafeUnwrapErr",
|
|
19
|
+
"isErr",
|
|
20
|
+
"isOk",
|
|
21
|
+
"match",
|
|
22
|
+
"matchTags",
|
|
23
|
+
"matchTagsPartial",
|
|
24
|
+
"unwrapOr",
|
|
25
|
+
"unwrapOrThrow"
|
|
26
|
+
]);
|
|
27
|
+
const consumerProperties = new Set(["error", "value"]);
|
|
28
|
+
const getUnionOrIntersectionTypes$1 = (tsApi, type) => (type.flags & (tsApi.TypeFlags.Union | tsApi.TypeFlags.Intersection)) === 0 ? void 0 : type.types ?? [];
|
|
29
|
+
const getSymbolName = (symbol) => {
|
|
30
|
+
if (symbol === void 0) return;
|
|
31
|
+
if (typeof symbol.getName === "function") return symbol.getName();
|
|
32
|
+
const { escapedName } = symbol;
|
|
33
|
+
return typeof escapedName === "string" ? escapedName : void 0;
|
|
34
|
+
};
|
|
35
|
+
const getTypeSymbolName = (type) => {
|
|
36
|
+
if (typeof type.getSymbol === "function") return getSymbolName(type.getSymbol());
|
|
37
|
+
return getSymbolName(type.symbol);
|
|
38
|
+
};
|
|
39
|
+
const getTokenPosOfNode$1 = (tsApi, node, sourceFile) => {
|
|
40
|
+
const getTokenPos = tsApi.getTokenPosOfNode;
|
|
41
|
+
return getTokenPos === void 0 ? node.pos : getTokenPos(node, sourceFile);
|
|
42
|
+
};
|
|
43
|
+
const getNodeStart$1 = (tsApi, node, sourceFile) => typeof node.getStart === "function" ? node.getStart(sourceFile) : getTokenPosOfNode$1(tsApi, node, sourceFile);
|
|
44
|
+
const getIdentifierText$1 = (identifier) => {
|
|
45
|
+
if (typeof identifier.text === "string") return identifier.text;
|
|
46
|
+
return identifier.escapedText === void 0 ? "" : String(identifier.escapedText);
|
|
47
|
+
};
|
|
48
|
+
const getNodeWidth$1 = (tsApi, node, sourceFile) => {
|
|
49
|
+
if (typeof node.getWidth === "function") return node.getWidth(sourceFile);
|
|
50
|
+
const start = getNodeStart$1(tsApi, node, sourceFile);
|
|
51
|
+
return node.end - start;
|
|
52
|
+
};
|
|
53
|
+
const getEntityNameText = (tsApi, name) => tsApi.isIdentifier(name) ? getIdentifierText$1(name) : `${getEntityNameText(tsApi, name.left)}.${getIdentifierText$1(name.right)}`;
|
|
54
|
+
const normalizeNoDiscardMode = (mode) => mode === "direct" ? "direct" : "must-use";
|
|
55
|
+
const isResultLikeType = (tsApi, checker, node, type) => {
|
|
56
|
+
const unionOrIntersectionTypes = getUnionOrIntersectionTypes$1(tsApi, type);
|
|
57
|
+
if (unionOrIntersectionTypes !== void 0) return unionOrIntersectionTypes.some((innerType) => isResultLikeType(tsApi, checker, node, innerType));
|
|
58
|
+
const aliasName = getSymbolName(type.aliasSymbol);
|
|
59
|
+
const symbolName = getTypeSymbolName(type);
|
|
60
|
+
if (aliasName !== void 0 && resultTypeNames.has(aliasName) || symbolName !== void 0 && resultTypeNames.has(symbolName)) return true;
|
|
61
|
+
if (tsApi.isTypeReferenceNode(node)) {
|
|
62
|
+
const typeName = getEntityNameText(tsApi, node.typeName);
|
|
63
|
+
const unqualifiedName = typeName.includes(".") ? typeName.split(".").at(-1) : typeName;
|
|
64
|
+
return unqualifiedName !== void 0 && resultTypeNames.has(unqualifiedName);
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
const unwrapExpression = (tsApi, expression) => {
|
|
69
|
+
let current = expression;
|
|
70
|
+
for (;;) if (tsApi.isParenthesizedExpression(current)) current = current.expression;
|
|
71
|
+
else if (tsApi.isAsExpression(current)) current = current.expression;
|
|
72
|
+
else if (tsApi.isTypeAssertionExpression(current)) current = current.expression;
|
|
73
|
+
else if (tsApi.isNonNullExpression(current)) current = current.expression;
|
|
74
|
+
else if (tsApi.isSatisfiesExpression(current)) current = current.expression;
|
|
75
|
+
else return current;
|
|
76
|
+
};
|
|
77
|
+
const isExplicitDiscard = (tsApi, expression) => tsApi.isVoidExpression(unwrapExpression(tsApi, expression));
|
|
78
|
+
const isCallLikeDiscard = (tsApi, expression) => {
|
|
79
|
+
const unwrapped = unwrapExpression(tsApi, expression);
|
|
80
|
+
if (tsApi.isAwaitExpression(unwrapped)) return isCallLikeDiscard(tsApi, unwrapped.expression);
|
|
81
|
+
if (tsApi.isCallExpression(unwrapped)) return true;
|
|
82
|
+
if (tsApi.isConditionalExpression(unwrapped)) return isCallLikeDiscard(tsApi, unwrapped.whenTrue) || isCallLikeDiscard(tsApi, unwrapped.whenFalse);
|
|
83
|
+
if (tsApi.isBinaryExpression(unwrapped)) {
|
|
84
|
+
const { kind } = unwrapped.operatorToken;
|
|
85
|
+
return (kind === tsApi.SyntaxKind.AmpersandAmpersandToken || kind === tsApi.SyntaxKind.BarBarToken || kind === tsApi.SyntaxKind.QuestionQuestionToken) && isCallLikeDiscard(tsApi, unwrapped.right);
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
};
|
|
89
|
+
const getTypeName$1 = (context, node, type) => context.checker.typeToString(type, node, context.tsApi.TypeFormatFlags.NoTruncation);
|
|
90
|
+
const createFinding$1 = (context, node, typeName, message) => {
|
|
91
|
+
const start = getNodeStart$1(context.tsApi, node, context.sourceFile);
|
|
92
|
+
const position = context.tsApi.getLineAndCharacterOfPosition(context.sourceFile, start);
|
|
93
|
+
return {
|
|
94
|
+
column: position.character + 1,
|
|
95
|
+
file: context.sourceFile.fileName,
|
|
96
|
+
length: getNodeWidth$1(context.tsApi, node, context.sourceFile),
|
|
97
|
+
line: position.line + 1,
|
|
98
|
+
message,
|
|
99
|
+
rule: "no-discard",
|
|
100
|
+
severity: "error",
|
|
101
|
+
start,
|
|
102
|
+
type: typeName
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
const createIgnoredFinding = (context, node, typeName) => createFinding$1(context, node, typeName, `Ignored ${typeName} value. Handle it or explicitly discard it with \`void\`.`);
|
|
106
|
+
const createUnhandledFinding = (context, tracked) => createFinding$1(context, tracked.identifier, tracked.typeName, `Unhandled ${tracked.typeName} value assigned to \`${tracked.name}\`. Handle it, return it, or explicitly discard it with \`void\`.`);
|
|
107
|
+
const symbolsEqual = (left, right) => left === right || left.valueDeclaration === right.valueDeclaration;
|
|
108
|
+
const isWrapperParent = (tsApi, parent, child) => tsApi.isParenthesizedExpression(parent) && parent.expression === child || tsApi.isAsExpression(parent) && parent.expression === child || tsApi.isTypeAssertionExpression(parent) && parent.expression === child || tsApi.isNonNullExpression(parent) && parent.expression === child || tsApi.isSatisfiesExpression(parent) && parent.expression === child;
|
|
109
|
+
const isReturnValueContainerParent = (tsApi, parent, child) => isWrapperParent(tsApi, parent, child) || tsApi.isShorthandPropertyAssignment(parent) && parent.name === child || tsApi.isPropertyAssignment(parent) && parent.initializer === child || tsApi.isSpreadAssignment(parent) && parent.expression === child || tsApi.isSpreadElement(parent) && parent.expression === child || tsApi.isObjectLiteralExpression(parent) && parent.properties.some((property) => property === child) || tsApi.isArrayLiteralExpression(parent) && parent.elements.some((element) => element === child) || tsApi.isConditionalExpression(parent) && (parent.whenTrue === child || parent.whenFalse === child);
|
|
110
|
+
const getReferenceChainRoot = (tsApi, identifier, ancestors) => {
|
|
111
|
+
let current = identifier;
|
|
112
|
+
let parentIndex = ancestors.length - 1;
|
|
113
|
+
for (;;) {
|
|
114
|
+
const parent = ancestors[parentIndex];
|
|
115
|
+
if (parent === void 0) return {
|
|
116
|
+
parent: void 0,
|
|
117
|
+
root: current
|
|
118
|
+
};
|
|
119
|
+
if (isWrapperParent(tsApi, parent, current)) current = parent;
|
|
120
|
+
else if (tsApi.isAwaitExpression(parent) && parent.expression === current) current = parent;
|
|
121
|
+
else if (tsApi.isPropertyAccessExpression(parent) && parent.expression === current) current = parent;
|
|
122
|
+
else if (tsApi.isCallExpression(parent) && parent.expression === current) current = parent;
|
|
123
|
+
else return {
|
|
124
|
+
parent,
|
|
125
|
+
root: current
|
|
126
|
+
};
|
|
127
|
+
parentIndex -= 1;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const isReturnedReference = (tsApi, identifier, ancestors) => {
|
|
131
|
+
const { parent, root } = getReferenceChainRoot(tsApi, identifier, ancestors);
|
|
132
|
+
if (parent !== void 0 && tsApi.isReturnStatement(parent) && parent.expression === root || parent !== void 0 && tsApi.isArrowFunction(parent) && parent.body === root) return true;
|
|
133
|
+
let current = identifier;
|
|
134
|
+
for (let parentIndex = ancestors.length - 1; parentIndex >= 0; parentIndex -= 1) {
|
|
135
|
+
const containerParent = ancestors[parentIndex];
|
|
136
|
+
if (containerParent === void 0) return false;
|
|
137
|
+
if (isReturnValueContainerParent(tsApi, containerParent, current)) {
|
|
138
|
+
current = containerParent;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
return tsApi.isReturnStatement(containerParent) && containerParent.expression === current || tsApi.isArrowFunction(containerParent) && containerParent.body === current;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
};
|
|
145
|
+
const isExplicitDiscardReference = (tsApi, identifier, ancestors) => {
|
|
146
|
+
const { parent, root } = getReferenceChainRoot(tsApi, identifier, ancestors);
|
|
147
|
+
return parent !== void 0 && tsApi.isVoidExpression(parent) && parent.expression === root;
|
|
148
|
+
};
|
|
149
|
+
const isConsumedByReceiverChain = (tsApi, identifier, ancestors) => {
|
|
150
|
+
let current = identifier;
|
|
151
|
+
let parentIndex = ancestors.length - 1;
|
|
152
|
+
for (;;) {
|
|
153
|
+
const parent = ancestors[parentIndex];
|
|
154
|
+
if (parent === void 0) return false;
|
|
155
|
+
if (isWrapperParent(tsApi, parent, current)) current = parent;
|
|
156
|
+
else if (tsApi.isAwaitExpression(parent) && parent.expression === current) current = parent;
|
|
157
|
+
else if (tsApi.isPropertyAccessExpression(parent) && parent.expression === current) {
|
|
158
|
+
const methodOrPropertyName = getIdentifierText$1(parent.name);
|
|
159
|
+
const nextParent = ancestors[parentIndex - 1];
|
|
160
|
+
if (consumerProperties.has(methodOrPropertyName)) return true;
|
|
161
|
+
if (consumerMethods.has(methodOrPropertyName) && nextParent !== void 0 && tsApi.isCallExpression(nextParent) && nextParent.expression === parent) return true;
|
|
162
|
+
current = parent;
|
|
163
|
+
} else if (tsApi.isCallExpression(parent) && parent.expression === current) current = parent;
|
|
164
|
+
else return false;
|
|
165
|
+
parentIndex -= 1;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const isHandledReference = (tsApi, identifier, ancestors) => isReturnedReference(tsApi, identifier, ancestors) || isExplicitDiscardReference(tsApi, identifier, ancestors) || isConsumedByReceiverChain(tsApi, identifier, ancestors);
|
|
169
|
+
const isIdentifierInsideDiscardedResultExpression = (context, identifier, ancestors) => {
|
|
170
|
+
let current = identifier;
|
|
171
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
172
|
+
current = ancestors[index];
|
|
173
|
+
if (context.tsApi.isExpressionStatement(current)) {
|
|
174
|
+
if (isExplicitDiscard(context.tsApi, current.expression) || !isCallLikeDiscard(context.tsApi, current.expression)) return false;
|
|
175
|
+
const type = context.checker.getTypeAtLocation(current.expression);
|
|
176
|
+
return isResultLikeType(context.tsApi, context.checker, current.expression, type);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
};
|
|
181
|
+
const getTrackedResult = (tsApi, checker, trackedResults, identifier) => {
|
|
182
|
+
const shorthandParent = identifier.parent;
|
|
183
|
+
const symbol = shorthandParent !== void 0 && tsApi.isShorthandPropertyAssignment(shorthandParent) && shorthandParent.name === identifier ? checker.getShorthandAssignmentValueSymbol(shorthandParent) : checker.getSymbolAtLocation(identifier);
|
|
184
|
+
return symbol === void 0 ? void 0 : trackedResults.find((tracked) => symbolsEqual(tracked.symbol, symbol));
|
|
185
|
+
};
|
|
186
|
+
const collectTrackedResults = (context) => {
|
|
187
|
+
const trackedResults = [];
|
|
188
|
+
const visit = (node) => {
|
|
189
|
+
if (context.tsApi.isVariableDeclaration(node) && context.tsApi.isIdentifier(node.name) && node.initializer !== void 0 && isCallLikeDiscard(context.tsApi, node.initializer)) {
|
|
190
|
+
const type = context.checker.getTypeAtLocation(node.initializer);
|
|
191
|
+
if (isResultLikeType(context.tsApi, context.checker, node.initializer, type)) {
|
|
192
|
+
const symbol = context.checker.getSymbolAtLocation(node.name);
|
|
193
|
+
if (symbol !== void 0) trackedResults.push({
|
|
194
|
+
declaration: node,
|
|
195
|
+
hasDiscardedResultUse: false,
|
|
196
|
+
handled: false,
|
|
197
|
+
identifier: node.name,
|
|
198
|
+
name: getIdentifierText$1(node.name),
|
|
199
|
+
symbol,
|
|
200
|
+
typeName: getTypeName$1(context, node.initializer, type)
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
context.tsApi.forEachChild(node, visit);
|
|
205
|
+
};
|
|
206
|
+
visit(context.sourceFile);
|
|
207
|
+
return trackedResults;
|
|
208
|
+
};
|
|
209
|
+
const markTrackedResultUses = (context, trackedResults) => {
|
|
210
|
+
const visit = (node, ancestors) => {
|
|
211
|
+
if (context.tsApi.isIdentifier(node)) {
|
|
212
|
+
const tracked = getTrackedResult(context.tsApi, context.checker, trackedResults, node);
|
|
213
|
+
if (tracked !== void 0 && node !== tracked.identifier) {
|
|
214
|
+
if (isHandledReference(context.tsApi, node, ancestors)) tracked.handled = true;
|
|
215
|
+
if (isIdentifierInsideDiscardedResultExpression(context, node, ancestors)) tracked.hasDiscardedResultUse = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
context.tsApi.forEachChild(node, (child) => visit(child, [...ancestors, node]));
|
|
219
|
+
};
|
|
220
|
+
visit(context.sourceFile, []);
|
|
221
|
+
};
|
|
222
|
+
const getDirectFindings = (context) => {
|
|
223
|
+
const findings = [];
|
|
224
|
+
const visit = (node) => {
|
|
225
|
+
if (context.tsApi.isExpressionStatement(node) && !isExplicitDiscard(context.tsApi, node.expression) && isCallLikeDiscard(context.tsApi, node.expression)) {
|
|
226
|
+
const type = context.checker.getTypeAtLocation(node.expression);
|
|
227
|
+
if (isResultLikeType(context.tsApi, context.checker, node.expression, type)) findings.push(createIgnoredFinding(context, node.expression, getTypeName$1(context, node.expression, type)));
|
|
228
|
+
}
|
|
229
|
+
context.tsApi.forEachChild(node, visit);
|
|
230
|
+
};
|
|
231
|
+
visit(context.sourceFile);
|
|
232
|
+
return findings;
|
|
233
|
+
};
|
|
234
|
+
const getMustUseFindings = (context) => {
|
|
235
|
+
const trackedResults = collectTrackedResults(context);
|
|
236
|
+
markTrackedResultUses(context, trackedResults);
|
|
237
|
+
return trackedResults.filter((tracked) => !tracked.handled && !tracked.hasDiscardedResultUse).map((tracked) => createUnhandledFinding(context, tracked));
|
|
238
|
+
};
|
|
239
|
+
const getSourceFileNoDiscardFindings = (tsApi, checker, sourceFile, options = {}) => {
|
|
240
|
+
const context = {
|
|
241
|
+
checker,
|
|
242
|
+
sourceFile,
|
|
243
|
+
tsApi
|
|
244
|
+
};
|
|
245
|
+
const mode = normalizeNoDiscardMode(options.mode);
|
|
246
|
+
const directFindings = getDirectFindings(context);
|
|
247
|
+
return mode === "must-use" ? [...directFindings, ...getMustUseFindings(context)] : directFindings;
|
|
248
|
+
};
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/rules-core.ts
|
|
251
|
+
const resultarRuleNames = [
|
|
252
|
+
"no-discard",
|
|
253
|
+
"prefer-map-err",
|
|
254
|
+
"prefer-and-then",
|
|
255
|
+
"typed-catch-mapper",
|
|
256
|
+
"no-unsafe-await",
|
|
257
|
+
"no-try-catch-in-safe-try",
|
|
258
|
+
"yield-star-in-safe-try",
|
|
259
|
+
"unsafe-result-type-assertion",
|
|
260
|
+
"prefer-tagged-error",
|
|
261
|
+
"tagged-error-name-match",
|
|
262
|
+
"no-tagged-error-constructor-override",
|
|
263
|
+
"no-useless-recovery"
|
|
264
|
+
];
|
|
265
|
+
const ruleOptionNameByRule = {
|
|
266
|
+
"no-discard": "noDiscard",
|
|
267
|
+
"no-tagged-error-constructor-override": "noTaggedErrorConstructorOverride",
|
|
268
|
+
"no-try-catch-in-safe-try": "noTryCatchInSafeTry",
|
|
269
|
+
"no-unsafe-await": "noUnsafeAwait",
|
|
270
|
+
"no-useless-recovery": "noUselessRecovery",
|
|
271
|
+
"prefer-and-then": "preferAndThen",
|
|
272
|
+
"prefer-map-err": "preferMapErr",
|
|
273
|
+
"prefer-tagged-error": "preferTaggedError",
|
|
274
|
+
"tagged-error-name-match": "taggedErrorNameMatch",
|
|
275
|
+
"typed-catch-mapper": "typedCatchMapper",
|
|
276
|
+
"unsafe-result-type-assertion": "unsafeResultTypeAssertion",
|
|
277
|
+
"yield-star-in-safe-try": "yieldStarInSafeTry"
|
|
278
|
+
};
|
|
279
|
+
const defaultResultarRulesOptions = {
|
|
280
|
+
noDiscard: "error",
|
|
281
|
+
noDiscardMode: "must-use",
|
|
282
|
+
noTaggedErrorConstructorOverride: "warning",
|
|
283
|
+
noTryCatchInSafeTry: "warning",
|
|
284
|
+
noUnsafeAwait: "off",
|
|
285
|
+
noUnsafeAwaitMode: "resultar-context",
|
|
286
|
+
noUselessRecovery: "warning",
|
|
287
|
+
preferAndThen: "warning",
|
|
288
|
+
preferMapErr: "warning",
|
|
289
|
+
preferTaggedError: "warning",
|
|
290
|
+
taggedErrorNameMatch: "warning",
|
|
291
|
+
typedCatchMapper: "warning",
|
|
292
|
+
unsafeResultTypeAssertion: "warning",
|
|
293
|
+
yieldStarInSafeTry: "warning"
|
|
294
|
+
};
|
|
295
|
+
resultarRuleNames.filter((ruleName) => ruleName !== "no-discard");
|
|
296
|
+
const recoveryMethods = new Set([
|
|
297
|
+
"catchReason",
|
|
298
|
+
"catchReasons",
|
|
299
|
+
"catchTag",
|
|
300
|
+
"catchTags",
|
|
301
|
+
"mapErr",
|
|
302
|
+
"orElse",
|
|
303
|
+
"unwrapReason"
|
|
304
|
+
]);
|
|
305
|
+
const tryMapperCallNames = new Set([
|
|
306
|
+
"fromThrowable",
|
|
307
|
+
"fromThrowableAsync",
|
|
308
|
+
"tryCatch",
|
|
309
|
+
"tryCatchAsync",
|
|
310
|
+
"tryResult",
|
|
311
|
+
"tryResultAsync",
|
|
312
|
+
"tryAsync"
|
|
313
|
+
]);
|
|
314
|
+
const asyncAwaitBoundaryCallNames = new Set([
|
|
315
|
+
"fromThrowableAsync",
|
|
316
|
+
"tryCatchAsync",
|
|
317
|
+
"tryResultAsync",
|
|
318
|
+
"tryAsync"
|
|
319
|
+
]);
|
|
320
|
+
const normalizeRuleSeverity = (value, fallback) => value === "error" || value === "message" || value === "off" || value === "suggestion" || value === "warning" ? value : fallback;
|
|
321
|
+
const normalizeNoUnsafeAwaitMode = (value, fallback = defaultResultarRulesOptions.noUnsafeAwaitMode) => value === "all" || value === "resultar-context" ? value : fallback;
|
|
322
|
+
const normalizeResultarRulesOptions = (options = {}) => ({
|
|
323
|
+
noDiscard: normalizeRuleSeverity(options.noDiscard, defaultResultarRulesOptions.noDiscard),
|
|
324
|
+
noDiscardMode: normalizeNoDiscardMode(options.noDiscardMode),
|
|
325
|
+
noTaggedErrorConstructorOverride: normalizeRuleSeverity(options.noTaggedErrorConstructorOverride, defaultResultarRulesOptions.noTaggedErrorConstructorOverride),
|
|
326
|
+
noTryCatchInSafeTry: normalizeRuleSeverity(options.noTryCatchInSafeTry, defaultResultarRulesOptions.noTryCatchInSafeTry),
|
|
327
|
+
noUnsafeAwait: normalizeRuleSeverity(options.noUnsafeAwait, defaultResultarRulesOptions.noUnsafeAwait),
|
|
328
|
+
noUnsafeAwaitMode: normalizeNoUnsafeAwaitMode(options.noUnsafeAwaitMode),
|
|
329
|
+
noUselessRecovery: normalizeRuleSeverity(options.noUselessRecovery, defaultResultarRulesOptions.noUselessRecovery),
|
|
330
|
+
preferAndThen: normalizeRuleSeverity(options.preferAndThen, defaultResultarRulesOptions.preferAndThen),
|
|
331
|
+
preferMapErr: normalizeRuleSeverity(options.preferMapErr, defaultResultarRulesOptions.preferMapErr),
|
|
332
|
+
preferTaggedError: normalizeRuleSeverity(options.preferTaggedError, defaultResultarRulesOptions.preferTaggedError),
|
|
333
|
+
taggedErrorNameMatch: normalizeRuleSeverity(options.taggedErrorNameMatch, defaultResultarRulesOptions.taggedErrorNameMatch),
|
|
334
|
+
typedCatchMapper: normalizeRuleSeverity(options.typedCatchMapper, defaultResultarRulesOptions.typedCatchMapper),
|
|
335
|
+
unsafeResultTypeAssertion: normalizeRuleSeverity(options.unsafeResultTypeAssertion, defaultResultarRulesOptions.unsafeResultTypeAssertion),
|
|
336
|
+
yieldStarInSafeTry: normalizeRuleSeverity(options.yieldStarInSafeTry, defaultResultarRulesOptions.yieldStarInSafeTry)
|
|
337
|
+
});
|
|
338
|
+
const getRuleSeverity = (options, rule) => {
|
|
339
|
+
const severity = options[ruleOptionNameByRule[rule]];
|
|
340
|
+
return severity === "off" ? void 0 : severity;
|
|
341
|
+
};
|
|
342
|
+
const getTokenPosOfNode = (context, node) => {
|
|
343
|
+
const getTokenPos = context.tsApi.getTokenPosOfNode;
|
|
344
|
+
return getTokenPos === void 0 ? node.pos : getTokenPos(node, context.sourceFile);
|
|
345
|
+
};
|
|
346
|
+
const getNodeStart = (context, node) => typeof node.getStart === "function" ? node.getStart(context.sourceFile) : getTokenPosOfNode(context, node);
|
|
347
|
+
const getIdentifierText = (identifier) => {
|
|
348
|
+
if (typeof identifier.text === "string") return identifier.text;
|
|
349
|
+
return identifier.escapedText === void 0 ? "" : String(identifier.escapedText);
|
|
350
|
+
};
|
|
351
|
+
const getNodeWidth = (context, node) => {
|
|
352
|
+
if (typeof node.getWidth === "function") return node.getWidth(context.sourceFile);
|
|
353
|
+
const start = getNodeStart(context, node);
|
|
354
|
+
return node.end - start;
|
|
355
|
+
};
|
|
356
|
+
const createFinding = (context, node, rule, severity, message, type) => {
|
|
357
|
+
const start = getNodeStart(context, node);
|
|
358
|
+
const position = context.tsApi.getLineAndCharacterOfPosition(context.sourceFile, start);
|
|
359
|
+
const base = {
|
|
360
|
+
column: position.character + 1,
|
|
361
|
+
file: context.sourceFile.fileName,
|
|
362
|
+
length: getNodeWidth(context, node),
|
|
363
|
+
line: position.line + 1,
|
|
364
|
+
message,
|
|
365
|
+
rule,
|
|
366
|
+
severity,
|
|
367
|
+
start
|
|
368
|
+
};
|
|
369
|
+
return type === void 0 ? base : {
|
|
370
|
+
...base,
|
|
371
|
+
type
|
|
372
|
+
};
|
|
373
|
+
};
|
|
374
|
+
const visitSourceFile = (context, visitor) => {
|
|
375
|
+
const visit = (node) => {
|
|
376
|
+
visitor(node);
|
|
377
|
+
context.tsApi.forEachChild(node, visit);
|
|
378
|
+
};
|
|
379
|
+
visit(context.sourceFile);
|
|
380
|
+
};
|
|
381
|
+
const getPropertyNameText = (tsApi, name) => {
|
|
382
|
+
if (tsApi.isIdentifier(name)) return getIdentifierText(name);
|
|
383
|
+
if (tsApi.isStringLiteral(name) || tsApi.isNumericLiteral(name)) return name.text;
|
|
384
|
+
};
|
|
385
|
+
const getExpressionName = (tsApi, expression) => {
|
|
386
|
+
const unwrapped = unwrapExpression(tsApi, expression);
|
|
387
|
+
if (tsApi.isIdentifier(unwrapped)) return getIdentifierText(unwrapped);
|
|
388
|
+
if (tsApi.isPropertyAccessExpression(unwrapped)) return getIdentifierText(unwrapped.name);
|
|
389
|
+
};
|
|
390
|
+
const getMethodCall = (tsApi, node) => {
|
|
391
|
+
if (!tsApi.isCallExpression(node)) return;
|
|
392
|
+
const expression = unwrapExpression(tsApi, node.expression);
|
|
393
|
+
if (!tsApi.isPropertyAccessExpression(expression)) return;
|
|
394
|
+
if (!tsApi.isIdentifier(expression.name)) return;
|
|
395
|
+
return {
|
|
396
|
+
methodName: getIdentifierText(expression.name),
|
|
397
|
+
nameNode: expression.name,
|
|
398
|
+
receiver: expression.expression
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
const getTypeName = (context, node, type) => context.checker.typeToString(type, node, context.tsApi.TypeFormatFlags.NoTruncation);
|
|
402
|
+
const getUnionOrIntersectionTypes = (context, type) => (type.flags & (context.tsApi.TypeFlags.Union | context.tsApi.TypeFlags.Intersection)) === 0 ? void 0 : type.types ?? [];
|
|
403
|
+
const getTypeArguments = (context, type) => {
|
|
404
|
+
const { aliasTypeArguments } = type;
|
|
405
|
+
if (aliasTypeArguments !== void 0 && aliasTypeArguments.length > 0) return aliasTypeArguments;
|
|
406
|
+
const reference = type;
|
|
407
|
+
return reference.target === void 0 ? [] : context.checker.getTypeArguments(reference);
|
|
408
|
+
};
|
|
409
|
+
const getResultTypeParts = (context, node, type) => {
|
|
410
|
+
const unionOrIntersectionTypes = getUnionOrIntersectionTypes(context, type);
|
|
411
|
+
if (unionOrIntersectionTypes !== void 0) return unionOrIntersectionTypes.flatMap((innerType) => getResultTypeParts(context, node, innerType));
|
|
412
|
+
if (!isResultLikeType(context.tsApi, context.checker, node, type)) return [];
|
|
413
|
+
const [okType, errorType] = getTypeArguments(context, type);
|
|
414
|
+
return [{
|
|
415
|
+
error: errorType,
|
|
416
|
+
ok: okType
|
|
417
|
+
}];
|
|
418
|
+
};
|
|
419
|
+
const isResultAsyncLikeType = (context, node, type) => {
|
|
420
|
+
const unionOrIntersectionTypes = getUnionOrIntersectionTypes(context, type);
|
|
421
|
+
if (unionOrIntersectionTypes !== void 0) return unionOrIntersectionTypes.some((innerType) => isResultAsyncLikeType(context, node, innerType));
|
|
422
|
+
return /\b(?:DisposableResultAsync|ResultAsync|StrictResultAsync)\b/.test(getTypeName(context, node, type));
|
|
423
|
+
};
|
|
424
|
+
const isResultLikeExpression = (context, expression) => {
|
|
425
|
+
const type = context.checker.getTypeAtLocation(expression);
|
|
426
|
+
return isResultLikeType(context.tsApi, context.checker, expression, type);
|
|
427
|
+
};
|
|
428
|
+
const getUnionTypes = (context, type) => (type.flags & context.tsApi.TypeFlags.Union) === 0 ? void 0 : type.types ?? [];
|
|
429
|
+
const everyUnionPart = (context, type, predicate) => {
|
|
430
|
+
const unionTypes = getUnionTypes(context, type);
|
|
431
|
+
return unionTypes === void 0 ? predicate(type) : unionTypes.every((innerType) => predicate(innerType));
|
|
432
|
+
};
|
|
433
|
+
const isResultAsyncLikeAwaitType = (context, node, type) => everyUnionPart(context, type, (innerType) => isResultAsyncLikeType(context, node, innerType));
|
|
434
|
+
const isResultLikeAwaitedType = (context, node, type) => everyUnionPart(context, type, (innerType) => isResultLikeType(context.tsApi, context.checker, node, innerType));
|
|
435
|
+
const getPromisedTypeOfPromise = (context, node, type) => {
|
|
436
|
+
return context.checker.getPromisedTypeOfPromise?.(type, node);
|
|
437
|
+
};
|
|
438
|
+
const isSafeAwaitExpression = (context, expression) => {
|
|
439
|
+
const expressionType = context.checker.getTypeAtLocation(expression);
|
|
440
|
+
if (isResultAsyncLikeAwaitType(context, expression, expressionType)) return true;
|
|
441
|
+
const promisedType = getPromisedTypeOfPromise(context, expression, expressionType);
|
|
442
|
+
if (promisedType === void 0) return true;
|
|
443
|
+
return isResultLikeAwaitedType(context, expression, context.checker.getAwaitedType(expressionType) ?? promisedType);
|
|
444
|
+
};
|
|
445
|
+
const isPromiseOfResultLikeType = (context, node, type) => {
|
|
446
|
+
const unionOrIntersectionTypes = getUnionOrIntersectionTypes(context, type);
|
|
447
|
+
if (unionOrIntersectionTypes !== void 0) return unionOrIntersectionTypes.some((innerType) => isPromiseOfResultLikeType(context, node, innerType));
|
|
448
|
+
const promisedType = getPromisedTypeOfPromise(context, node, type);
|
|
449
|
+
if (promisedType === void 0) return false;
|
|
450
|
+
return isResultLikeAwaitedType(context, node, context.checker.getAwaitedType(type) ?? promisedType);
|
|
451
|
+
};
|
|
452
|
+
const isResultarAsyncContextReturnType = (context, node, type) => isResultAsyncLikeType(context, node, type) || isPromiseOfResultLikeType(context, node, type);
|
|
453
|
+
const getFunctionLikeReturnType = (context, node) => {
|
|
454
|
+
const signature = context.checker.getSignatureFromDeclaration(node);
|
|
455
|
+
if (signature !== void 0) return signature.getReturnType();
|
|
456
|
+
const [callSignature] = context.checker.getTypeAtLocation(node).getCallSignatures();
|
|
457
|
+
return callSignature?.getReturnType();
|
|
458
|
+
};
|
|
459
|
+
const isResultarAsyncFunctionContext = (context, node) => {
|
|
460
|
+
if (!isFunctionLike(context.tsApi, node)) return false;
|
|
461
|
+
const returnType = getFunctionLikeReturnType(context, node);
|
|
462
|
+
return returnType !== void 0 && isResultarAsyncContextReturnType(context, node, returnType);
|
|
463
|
+
};
|
|
464
|
+
const isUnknownOrAnyType = (tsApi, type) => type !== void 0 && (Boolean(type.flags & tsApi.TypeFlags.Unknown) || Boolean(type.flags & tsApi.TypeFlags.Any));
|
|
465
|
+
const isNeverType = (tsApi, type) => type !== void 0 && Boolean(type.flags & tsApi.TypeFlags.Never);
|
|
466
|
+
const getReturnedExpressions = (tsApi, callback) => {
|
|
467
|
+
const expressions = [];
|
|
468
|
+
if (!tsApi.isArrowFunction(callback) && !tsApi.isFunctionExpression(callback)) return expressions;
|
|
469
|
+
if (tsApi.isArrowFunction(callback) && !tsApi.isBlock(callback.body)) return [callback.body];
|
|
470
|
+
const { body } = callback;
|
|
471
|
+
const visit = (node) => {
|
|
472
|
+
if (node !== body && isFunctionLike(tsApi, node)) return;
|
|
473
|
+
if (tsApi.isReturnStatement(node) && node.expression !== void 0) {
|
|
474
|
+
expressions.push(node.expression);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
tsApi.forEachChild(node, visit);
|
|
478
|
+
};
|
|
479
|
+
visit(body);
|
|
480
|
+
return expressions;
|
|
481
|
+
};
|
|
482
|
+
const isFunctionLike = (tsApi, node) => tsApi.isArrowFunction(node) || tsApi.isFunctionDeclaration(node) || tsApi.isFunctionExpression(node) || tsApi.isMethodDeclaration(node);
|
|
483
|
+
const isErrConstructorCall = (tsApi, expression) => {
|
|
484
|
+
const unwrapped = unwrapExpression(tsApi, expression);
|
|
485
|
+
if (!tsApi.isCallExpression(unwrapped)) return false;
|
|
486
|
+
return getExpressionName(tsApi, unwrapped.expression) === "err";
|
|
487
|
+
};
|
|
488
|
+
const hasObjectCatchProperty = (tsApi, expression) => {
|
|
489
|
+
const unwrapped = unwrapExpression(tsApi, expression);
|
|
490
|
+
if (!tsApi.isObjectLiteralExpression(unwrapped)) return false;
|
|
491
|
+
return unwrapped.properties.some((property) => {
|
|
492
|
+
if (tsApi.isPropertyAssignment(property) || tsApi.isMethodDeclaration(property)) return getPropertyNameText(tsApi, property.name) === "catch";
|
|
493
|
+
return false;
|
|
494
|
+
});
|
|
495
|
+
};
|
|
496
|
+
const hasMapperArgument = (tsApi, call) => {
|
|
497
|
+
const [firstArgument, secondArgument] = call.arguments;
|
|
498
|
+
return secondArgument !== void 0 || firstArgument !== void 0 && hasObjectCatchProperty(tsApi, firstArgument);
|
|
499
|
+
};
|
|
500
|
+
const getObjectTryBody = (tsApi, objectLiteral) => {
|
|
501
|
+
for (const property of objectLiteral.properties) {
|
|
502
|
+
if (tsApi.isMethodDeclaration(property) && getPropertyNameText(tsApi, property.name) === "try") return property;
|
|
503
|
+
if (tsApi.isPropertyAssignment(property) && getPropertyNameText(tsApi, property.name) === "try" && (tsApi.isArrowFunction(property.initializer) || tsApi.isFunctionExpression(property.initializer))) return property.initializer;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
const getSafeTryBody = (tsApi, call) => {
|
|
507
|
+
if (getExpressionName(tsApi, call.expression) !== "safeTry") return;
|
|
508
|
+
const [firstArgument] = call.arguments;
|
|
509
|
+
if (firstArgument === void 0) return;
|
|
510
|
+
const unwrapped = unwrapExpression(tsApi, firstArgument);
|
|
511
|
+
if (tsApi.isArrowFunction(unwrapped) || tsApi.isFunctionExpression(unwrapped)) return unwrapped;
|
|
512
|
+
if (!tsApi.isObjectLiteralExpression(unwrapped)) return;
|
|
513
|
+
return getObjectTryBody(tsApi, unwrapped);
|
|
514
|
+
};
|
|
515
|
+
const getResultarAwaitBoundaryBody = (tsApi, call) => {
|
|
516
|
+
const callName = getExpressionName(tsApi, call.expression);
|
|
517
|
+
if (callName === void 0 || !asyncAwaitBoundaryCallNames.has(callName)) return;
|
|
518
|
+
const [firstArgument] = call.arguments;
|
|
519
|
+
if (firstArgument === void 0) return;
|
|
520
|
+
const unwrapped = unwrapExpression(tsApi, firstArgument);
|
|
521
|
+
if (tsApi.isArrowFunction(unwrapped) || tsApi.isFunctionExpression(unwrapped)) return unwrapped;
|
|
522
|
+
if (tsApi.isObjectLiteralExpression(unwrapped)) return getObjectTryBody(tsApi, unwrapped);
|
|
523
|
+
};
|
|
524
|
+
const getResultarAwaitContextBody = (tsApi, call) => getSafeTryBody(tsApi, call) ?? getResultarAwaitBoundaryBody(tsApi, call);
|
|
525
|
+
const visitSafeTryBody = (tsApi, body, visitor) => {
|
|
526
|
+
const root = body.body;
|
|
527
|
+
if (root === void 0) return;
|
|
528
|
+
const visit = (node) => {
|
|
529
|
+
if (node !== root && isFunctionLike(tsApi, node)) return;
|
|
530
|
+
visitor(node);
|
|
531
|
+
tsApi.forEachChild(node, visit);
|
|
532
|
+
};
|
|
533
|
+
visit(root);
|
|
534
|
+
};
|
|
535
|
+
const getCreateTaggedErrorOptions = (tsApi, node) => {
|
|
536
|
+
for (const heritageClause of node.heritageClauses ?? []) {
|
|
537
|
+
if (heritageClause.token !== tsApi.SyntaxKind.ExtendsKeyword) continue;
|
|
538
|
+
for (const heritageType of heritageClause.types) {
|
|
539
|
+
const { expression } = heritageType;
|
|
540
|
+
if (tsApi.isCallExpression(expression) && getExpressionName(tsApi, expression.expression) === "createTaggedError") {
|
|
541
|
+
const [options] = expression.arguments;
|
|
542
|
+
return options !== void 0 && tsApi.isObjectLiteralExpression(options) ? options : void 0;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
const getTaggedErrorName = (tsApi, options) => {
|
|
548
|
+
for (const property of options.properties) if (tsApi.isPropertyAssignment(property) && getPropertyNameText(tsApi, property.name) === "name" && (tsApi.isStringLiteral(property.initializer) || tsApi.isNoSubstitutionTemplateLiteral(property.initializer))) return property.initializer;
|
|
549
|
+
};
|
|
550
|
+
const classExtendsNativeError = (tsApi, node) => (node.heritageClauses ?? []).some((heritageClause) => heritageClause.token === tsApi.SyntaxKind.ExtendsKeyword && heritageClause.types.some((heritageType) => getExpressionName(tsApi, heritageType.expression) === "Error"));
|
|
551
|
+
const getFirstResultErrorTypes = (context, node, type) => getResultTypeParts(context, node, type).map((part) => part.error).filter((errorType) => errorType !== void 0);
|
|
552
|
+
const getPreferMapErrFindings = (context, severity) => {
|
|
553
|
+
const findings = [];
|
|
554
|
+
visitSourceFile(context, (node) => {
|
|
555
|
+
const call = context.tsApi.isCallExpression(node) ? node : void 0;
|
|
556
|
+
const methodCall = getMethodCall(context.tsApi, node);
|
|
557
|
+
if (call === void 0 || methodCall?.methodName !== "orElse") return;
|
|
558
|
+
if (!isResultLikeExpression(context, methodCall.receiver)) return;
|
|
559
|
+
const [callback] = call.arguments;
|
|
560
|
+
const returnedExpressions = callback === void 0 ? [] : getReturnedExpressions(context.tsApi, callback);
|
|
561
|
+
if (returnedExpressions.length === 0 || !returnedExpressions.every((expression) => isErrConstructorCall(context.tsApi, expression) && isResultLikeExpression(context, expression))) return;
|
|
562
|
+
findings.push(createFinding(context, methodCall.nameNode, "prefer-map-err", severity, "`orElse` only replaces the failure with another Err. Use `mapErr` when the Ok value cannot recover."));
|
|
563
|
+
});
|
|
564
|
+
return findings;
|
|
565
|
+
};
|
|
566
|
+
const getPreferAndThenFindings = (context, severity) => {
|
|
567
|
+
const findings = [];
|
|
568
|
+
visitSourceFile(context, (node) => {
|
|
569
|
+
const call = context.tsApi.isCallExpression(node) ? node : void 0;
|
|
570
|
+
const methodCall = getMethodCall(context.tsApi, node);
|
|
571
|
+
if (call === void 0 || methodCall?.methodName !== "map") return;
|
|
572
|
+
const receiverType = context.checker.getTypeAtLocation(methodCall.receiver);
|
|
573
|
+
if (!isResultLikeType(context.tsApi, context.checker, methodCall.receiver, receiverType)) return;
|
|
574
|
+
const [callback] = call.arguments;
|
|
575
|
+
const returnedResult = (callback === void 0 ? [] : getReturnedExpressions(context.tsApi, callback)).find((expression) => isResultLikeExpression(context, expression));
|
|
576
|
+
if (returnedResult === void 0) return;
|
|
577
|
+
const returnedType = context.checker.getTypeAtLocation(returnedResult);
|
|
578
|
+
const methodName = !isResultAsyncLikeType(context, methodCall.receiver, receiverType) && isResultAsyncLikeType(context, returnedResult, returnedType) ? "asyncAndThen" : "andThen";
|
|
579
|
+
findings.push(createFinding(context, methodCall.nameNode, "prefer-and-then", severity, `\`map\` creates a nested Result when its callback returns ${getTypeName(context, returnedResult, returnedType)}. Use \`${methodName}\` for fallible composition.`));
|
|
580
|
+
});
|
|
581
|
+
return findings;
|
|
582
|
+
};
|
|
583
|
+
const getTypedCatchMapperFindings = (context, severity) => {
|
|
584
|
+
const findings = [];
|
|
585
|
+
visitSourceFile(context, (node) => {
|
|
586
|
+
if (!context.tsApi.isCallExpression(node)) return;
|
|
587
|
+
const callName = getExpressionName(context.tsApi, node.expression);
|
|
588
|
+
if (callName === void 0 || !tryMapperCallNames.has(callName) || hasMapperArgument(context.tsApi, node)) return;
|
|
589
|
+
if (callName !== "fromThrowable" && callName !== "fromThrowableAsync") {
|
|
590
|
+
const errorTypes = getFirstResultErrorTypes(context, node, context.checker.getTypeAtLocation(node));
|
|
591
|
+
if (errorTypes.length > 0 && errorTypes.every((errorType) => !isUnknownOrAnyType(context.tsApi, errorType))) return;
|
|
592
|
+
}
|
|
593
|
+
findings.push(createFinding(context, node.expression, "typed-catch-mapper", severity, `\`${callName}\` without a catch mapper leaves the error channel as \`unknown\`. Map the caught value to a specific Resultar error.`));
|
|
594
|
+
});
|
|
595
|
+
return findings;
|
|
596
|
+
};
|
|
597
|
+
const collectResultarAwaitBoundaryBodies = (context) => {
|
|
598
|
+
const bodies = /* @__PURE__ */ new Set();
|
|
599
|
+
visitSourceFile(context, (node) => {
|
|
600
|
+
if (!context.tsApi.isCallExpression(node)) return;
|
|
601
|
+
const body = getResultarAwaitBoundaryBody(context.tsApi, node);
|
|
602
|
+
if (body !== void 0) bodies.add(body);
|
|
603
|
+
});
|
|
604
|
+
return bodies;
|
|
605
|
+
};
|
|
606
|
+
const collectResultarAwaitContextBodies = (context) => {
|
|
607
|
+
const bodies = /* @__PURE__ */ new Set();
|
|
608
|
+
visitSourceFile(context, (node) => {
|
|
609
|
+
if (!context.tsApi.isCallExpression(node)) return;
|
|
610
|
+
const body = getResultarAwaitContextBody(context.tsApi, node);
|
|
611
|
+
if (body !== void 0) bodies.add(body);
|
|
612
|
+
});
|
|
613
|
+
return bodies;
|
|
614
|
+
};
|
|
615
|
+
const getNoUnsafeAwaitFindings = (context, severity, mode) => {
|
|
616
|
+
const findings = [];
|
|
617
|
+
const boundaryBodies = collectResultarAwaitBoundaryBodies(context);
|
|
618
|
+
const contextBodies = collectResultarAwaitContextBodies(context);
|
|
619
|
+
const visit = (node, insideResultarBoundary, insideResultarContext) => {
|
|
620
|
+
const startsResultarBoundary = boundaryBodies.has(node);
|
|
621
|
+
const currentInsideBoundary = insideResultarBoundary || startsResultarBoundary;
|
|
622
|
+
const startsResultarContext = contextBodies.has(node) || isResultarAsyncFunctionContext(context, node);
|
|
623
|
+
const currentInsideContext = insideResultarContext || startsResultarContext;
|
|
624
|
+
if ((mode === "all" || currentInsideContext) && context.tsApi.isAwaitExpression(node) && !currentInsideBoundary && !isSafeAwaitExpression(context, node.expression)) findings.push(createFinding(context, node, "no-unsafe-await", severity, "Wrap this awaited Promise in tryAsync, tryResultAsync, tryCatchAsync, or fromThrowableAsync so rejections stay in the Resultar error channel."));
|
|
625
|
+
if (node !== context.sourceFile && isFunctionLike(context.tsApi, node)) {
|
|
626
|
+
context.tsApi.forEachChild(node, (child) => visit(child, startsResultarBoundary, startsResultarContext));
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
context.tsApi.forEachChild(node, (child) => visit(child, currentInsideBoundary, currentInsideContext));
|
|
630
|
+
};
|
|
631
|
+
visit(context.sourceFile, false, false);
|
|
632
|
+
return findings;
|
|
633
|
+
};
|
|
634
|
+
const getNoTryCatchInSafeTryFindings = (context, severity) => {
|
|
635
|
+
const findings = [];
|
|
636
|
+
visitSourceFile(context, (node) => {
|
|
637
|
+
if (!context.tsApi.isCallExpression(node)) return;
|
|
638
|
+
const safeTryBody = getSafeTryBody(context.tsApi, node);
|
|
639
|
+
if (safeTryBody === void 0) return;
|
|
640
|
+
visitSafeTryBody(context.tsApi, safeTryBody, (bodyNode) => {
|
|
641
|
+
if (context.tsApi.isTryStatement(bodyNode)) findings.push(createFinding(context, bodyNode, "no-try-catch-in-safe-try", severity, "Avoid raw try/catch inside `safeTry`. Use `safeTry({ try, catch })`, `tryResult`, or `tryResultAsync` to keep failures typed."));
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
return findings;
|
|
645
|
+
};
|
|
646
|
+
const getYieldStarInSafeTryFindings = (context, severity) => {
|
|
647
|
+
const findings = [];
|
|
648
|
+
visitSourceFile(context, (node) => {
|
|
649
|
+
if (!context.tsApi.isCallExpression(node)) return;
|
|
650
|
+
const safeTryBody = getSafeTryBody(context.tsApi, node);
|
|
651
|
+
if (safeTryBody === void 0) return;
|
|
652
|
+
visitSafeTryBody(context.tsApi, safeTryBody, (bodyNode) => {
|
|
653
|
+
if (context.tsApi.isYieldExpression(bodyNode) && bodyNode.asteriskToken === void 0 && bodyNode.expression !== void 0 && isResultLikeExpression(context, bodyNode.expression)) findings.push(createFinding(context, bodyNode, "yield-star-in-safe-try", severity, "Use `yield*` when unwrapping Resultar values inside `safeTry`."));
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
return findings;
|
|
657
|
+
};
|
|
658
|
+
const getUnsafeResultTypeAssertionFindings = (context, severity) => {
|
|
659
|
+
const findings = [];
|
|
660
|
+
visitSourceFile(context, (node) => {
|
|
661
|
+
if (!context.tsApi.isAsExpression(node) && !context.tsApi.isTypeAssertionExpression(node)) return;
|
|
662
|
+
const { expression } = node;
|
|
663
|
+
const originalType = context.checker.getTypeAtLocation(expression);
|
|
664
|
+
const assertedType = context.checker.getTypeAtLocation(node);
|
|
665
|
+
const originalParts = getResultTypeParts(context, expression, originalType);
|
|
666
|
+
const assertedParts = getResultTypeParts(context, node, assertedType);
|
|
667
|
+
if (originalParts.length === 0 || assertedParts.length === 0) return;
|
|
668
|
+
const narrowedErrors = originalParts.flatMap((originalPart) => assertedParts.filter((assertedPart) => originalPart.error !== void 0 && assertedPart.error !== void 0 && !isUnknownOrAnyType(context.tsApi, originalPart.error) && !context.checker.isTypeAssignableTo(originalPart.error, assertedPart.error)).map((assertedPart) => ({
|
|
669
|
+
asserted: assertedPart.error,
|
|
670
|
+
original: originalPart.error
|
|
671
|
+
})));
|
|
672
|
+
if (narrowedErrors.length === 0) return;
|
|
673
|
+
const details = narrowedErrors.map(({ asserted, original }) => `\`${getTypeName(context, expression, original)}\` to \`${getTypeName(context, node, asserted)}\``).join(", ");
|
|
674
|
+
findings.push(createFinding(context, node, "unsafe-result-type-assertion", severity, `This assertion narrows the Resultar error channel unsafely (${details}). Prefer a real recovery or mapping step.`));
|
|
675
|
+
});
|
|
676
|
+
return findings;
|
|
677
|
+
};
|
|
678
|
+
const getPreferTaggedErrorFindings = (context, severity) => {
|
|
679
|
+
const findings = [];
|
|
680
|
+
visitSourceFile(context, (node) => {
|
|
681
|
+
if (context.tsApi.isClassDeclaration(node) && classExtendsNativeError(context.tsApi, node)) {
|
|
682
|
+
findings.push(createFinding(context, node.name ?? node, "prefer-tagged-error", severity, "Prefer `createTaggedError` for Resultar domain errors so failures keep a stable tag and typed metadata."));
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (!context.tsApi.isCallExpression(node) || getExpressionName(context.tsApi, node.expression) !== "err") return;
|
|
686
|
+
const [errorArgument] = node.arguments;
|
|
687
|
+
if (errorArgument === void 0) return;
|
|
688
|
+
const unwrappedErrorArgument = unwrapExpression(context.tsApi, errorArgument);
|
|
689
|
+
if (context.tsApi.isNewExpression(unwrappedErrorArgument) && getExpressionName(context.tsApi, unwrappedErrorArgument.expression) === "Error") findings.push(createFinding(context, errorArgument, "prefer-tagged-error", severity, "Prefer a `createTaggedError` instance over `new Error(...)` in Resultar error channels."));
|
|
690
|
+
});
|
|
691
|
+
return findings;
|
|
692
|
+
};
|
|
693
|
+
const getTaggedErrorNameMatchFindings = (context, severity) => {
|
|
694
|
+
const findings = [];
|
|
695
|
+
visitSourceFile(context, (node) => {
|
|
696
|
+
if (!context.tsApi.isClassDeclaration(node) || node.name === void 0) return;
|
|
697
|
+
const options = getCreateTaggedErrorOptions(context.tsApi, node);
|
|
698
|
+
const nameInitializer = options === void 0 ? void 0 : getTaggedErrorName(context.tsApi, options);
|
|
699
|
+
const className = getIdentifierText(node.name);
|
|
700
|
+
if (nameInitializer === void 0 || nameInitializer.text === className) return;
|
|
701
|
+
findings.push(createFinding(context, nameInitializer, "tagged-error-name-match", severity, `Tagged error name \`${nameInitializer.text}\` should match class name \`${className}\`.`));
|
|
702
|
+
});
|
|
703
|
+
return findings;
|
|
704
|
+
};
|
|
705
|
+
const getNoTaggedErrorConstructorOverrideFindings = (context, severity) => {
|
|
706
|
+
const findings = [];
|
|
707
|
+
visitSourceFile(context, (node) => {
|
|
708
|
+
if (!context.tsApi.isClassDeclaration(node) || getCreateTaggedErrorOptions(context.tsApi, node) === void 0) return;
|
|
709
|
+
for (const member of node.members) if (context.tsApi.isConstructorDeclaration(member)) findings.push(createFinding(context, member, "no-tagged-error-constructor-override", severity, "Do not override the constructor generated by `createTaggedError`; it owns template props, cause, and serialization behavior."));
|
|
710
|
+
});
|
|
711
|
+
return findings;
|
|
712
|
+
};
|
|
713
|
+
const getNoUselessRecoveryFindings = (context, severity) => {
|
|
714
|
+
const findings = [];
|
|
715
|
+
visitSourceFile(context, (node) => {
|
|
716
|
+
const methodCall = getMethodCall(context.tsApi, node);
|
|
717
|
+
if (methodCall === void 0 || !recoveryMethods.has(methodCall.methodName)) return;
|
|
718
|
+
const receiverType = context.checker.getTypeAtLocation(methodCall.receiver);
|
|
719
|
+
const errorTypes = getFirstResultErrorTypes(context, methodCall.receiver, receiverType);
|
|
720
|
+
if (errorTypes.length === 0 || !errorTypes.every((errorType) => isNeverType(context.tsApi, errorType))) return;
|
|
721
|
+
findings.push(createFinding(context, methodCall.nameNode, "no-useless-recovery", severity, `\`${methodCall.methodName}\` cannot run because this Resultar value has \`never\` in the error channel.`));
|
|
722
|
+
});
|
|
723
|
+
return findings;
|
|
724
|
+
};
|
|
725
|
+
const getSourceFileResultarFindings = (tsApi, checker, sourceFile, options = {}) => {
|
|
726
|
+
const normalizedOptions = normalizeResultarRulesOptions(options);
|
|
727
|
+
const context = {
|
|
728
|
+
checker,
|
|
729
|
+
sourceFile,
|
|
730
|
+
tsApi
|
|
731
|
+
};
|
|
732
|
+
const findings = [];
|
|
733
|
+
const noDiscardSeverity = getRuleSeverity(normalizedOptions, "no-discard");
|
|
734
|
+
if (noDiscardSeverity !== void 0) findings.push(...getSourceFileNoDiscardFindings(tsApi, checker, sourceFile, { mode: normalizedOptions.noDiscardMode }).map((finding) => ({
|
|
735
|
+
...finding,
|
|
736
|
+
severity: noDiscardSeverity
|
|
737
|
+
})));
|
|
738
|
+
const noUnsafeAwaitSeverity = getRuleSeverity(normalizedOptions, "no-unsafe-await");
|
|
739
|
+
if (noUnsafeAwaitSeverity !== void 0) findings.push(...getNoUnsafeAwaitFindings(context, noUnsafeAwaitSeverity, normalizedOptions.noUnsafeAwaitMode));
|
|
740
|
+
const ruleFns = [
|
|
741
|
+
["prefer-map-err", getPreferMapErrFindings],
|
|
742
|
+
["prefer-and-then", getPreferAndThenFindings],
|
|
743
|
+
["typed-catch-mapper", getTypedCatchMapperFindings],
|
|
744
|
+
["no-try-catch-in-safe-try", getNoTryCatchInSafeTryFindings],
|
|
745
|
+
["yield-star-in-safe-try", getYieldStarInSafeTryFindings],
|
|
746
|
+
["unsafe-result-type-assertion", getUnsafeResultTypeAssertionFindings],
|
|
747
|
+
["prefer-tagged-error", getPreferTaggedErrorFindings],
|
|
748
|
+
["tagged-error-name-match", getTaggedErrorNameMatchFindings],
|
|
749
|
+
["no-tagged-error-constructor-override", getNoTaggedErrorConstructorOverrideFindings],
|
|
750
|
+
["no-useless-recovery", getNoUselessRecoveryFindings]
|
|
751
|
+
];
|
|
752
|
+
for (const [ruleName, ruleFn] of ruleFns) {
|
|
753
|
+
const severity = getRuleSeverity(normalizedOptions, ruleName);
|
|
754
|
+
if (severity !== void 0) findings.push(...ruleFn(context, severity));
|
|
755
|
+
}
|
|
756
|
+
return findings.toSorted((left, right) => left.start - right.start || left.rule.localeCompare(right.rule));
|
|
757
|
+
};
|
|
758
|
+
const getProgramResultarFindings = (tsApi, program, options = {}) => {
|
|
759
|
+
const checker = program.getTypeChecker();
|
|
760
|
+
const findings = [];
|
|
761
|
+
for (const sourceFile of program.getSourceFiles()) if (!sourceFile.isDeclarationFile && !sourceFile.fileName.includes("/node_modules/") && !sourceFile.fileName.includes("\\node_modules\\")) findings.push(...getSourceFileResultarFindings(tsApi, checker, sourceFile, options));
|
|
762
|
+
return findings;
|
|
763
|
+
};
|
|
764
|
+
//#endregion
|
|
765
|
+
//#region src/plugin-options.ts
|
|
766
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null;
|
|
767
|
+
const isResultarPluginName = (value) => value === "resultar-check" || value === "resultar-lint";
|
|
768
|
+
const parsePluginOptions = (config) => {
|
|
769
|
+
if (!isRecord$1(config)) return defaultResultarRulesOptions;
|
|
770
|
+
return {
|
|
771
|
+
noDiscard: normalizeRuleSeverity(config.noDiscard, defaultResultarRulesOptions.noDiscard),
|
|
772
|
+
noDiscardMode: normalizeNoDiscardMode(config.noDiscardMode),
|
|
773
|
+
noTaggedErrorConstructorOverride: normalizeRuleSeverity(config.noTaggedErrorConstructorOverride, defaultResultarRulesOptions.noTaggedErrorConstructorOverride),
|
|
774
|
+
noTryCatchInSafeTry: normalizeRuleSeverity(config.noTryCatchInSafeTry, defaultResultarRulesOptions.noTryCatchInSafeTry),
|
|
775
|
+
noUnsafeAwait: normalizeRuleSeverity(config.noUnsafeAwait, defaultResultarRulesOptions.noUnsafeAwait),
|
|
776
|
+
noUnsafeAwaitMode: normalizeNoUnsafeAwaitMode(config.noUnsafeAwaitMode),
|
|
777
|
+
noUselessRecovery: normalizeRuleSeverity(config.noUselessRecovery, defaultResultarRulesOptions.noUselessRecovery),
|
|
778
|
+
preferAndThen: normalizeRuleSeverity(config.preferAndThen, defaultResultarRulesOptions.preferAndThen),
|
|
779
|
+
preferMapErr: normalizeRuleSeverity(config.preferMapErr, defaultResultarRulesOptions.preferMapErr),
|
|
780
|
+
preferTaggedError: normalizeRuleSeverity(config.preferTaggedError, defaultResultarRulesOptions.preferTaggedError),
|
|
781
|
+
taggedErrorNameMatch: normalizeRuleSeverity(config.taggedErrorNameMatch, defaultResultarRulesOptions.taggedErrorNameMatch),
|
|
782
|
+
typedCatchMapper: normalizeRuleSeverity(config.typedCatchMapper, defaultResultarRulesOptions.typedCatchMapper),
|
|
783
|
+
unsafeResultTypeAssertion: normalizeRuleSeverity(config.unsafeResultTypeAssertion, defaultResultarRulesOptions.unsafeResultTypeAssertion),
|
|
784
|
+
yieldStarInSafeTry: normalizeRuleSeverity(config.yieldStarInSafeTry, defaultResultarRulesOptions.yieldStarInSafeTry)
|
|
785
|
+
};
|
|
786
|
+
};
|
|
787
|
+
const findResultarPluginConfig = (plugins) => {
|
|
788
|
+
const plugin = plugins?.find((entry) => isRecord$1(entry) && isResultarPluginName(entry.name));
|
|
789
|
+
return plugin === void 0 ? void 0 : parsePluginOptions(plugin);
|
|
790
|
+
};
|
|
791
|
+
//#endregion
|
|
792
|
+
//#region src/lint.ts
|
|
793
|
+
const usage = `Usage: resultar-check -p tsconfig.json --noEmit
|
|
794
|
+
|
|
795
|
+
Flags:
|
|
796
|
+
--mode <direct|must-use> Check mode. Defaults to tsconfig plugin noDiscardMode or must-use.
|
|
797
|
+
-p, --project <path> TypeScript project file to inspect. Defaults to tsconfig.json.
|
|
798
|
+
-h, --help Show this help message.
|
|
799
|
+
|
|
800
|
+
Runs TypeScript 7 first, then all enabled Resultar rules from tsconfig plugin options.
|
|
801
|
+
`;
|
|
802
|
+
const failure = (error) => ({
|
|
803
|
+
error,
|
|
804
|
+
ok: false
|
|
805
|
+
});
|
|
806
|
+
const success = (findings) => ({
|
|
807
|
+
findings,
|
|
808
|
+
ok: true
|
|
809
|
+
});
|
|
810
|
+
const cliError = (message) => failure(new Error(message));
|
|
811
|
+
const requireFromPackage = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
812
|
+
const isTypeScript7Version = (version) => version.startsWith("7.");
|
|
813
|
+
const resolvePackageFromRoot = (rootDir, specifier) => {
|
|
814
|
+
const requireFromRoot = (0, node_module.createRequire)((0, node_path.resolve)(rootDir, "package.json"));
|
|
815
|
+
try {
|
|
816
|
+
return requireFromRoot.resolve(specifier);
|
|
817
|
+
} catch {
|
|
818
|
+
return requireFromPackage.resolve(specifier);
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
const readPackageVersion = (packageJson) => {
|
|
822
|
+
const parsed = JSON.parse((0, node_fs.readFileSync)(packageJson, "utf8"));
|
|
823
|
+
if (!isRecord(parsed) || typeof parsed.version !== "string") throw new TypeError(`Unable to read package version from ${packageJson}`);
|
|
824
|
+
return parsed.version;
|
|
825
|
+
};
|
|
826
|
+
const resolveOptionalPackage = (rootDir, specifier) => {
|
|
827
|
+
try {
|
|
828
|
+
return resolvePackageFromRoot(rootDir, specifier);
|
|
829
|
+
} catch {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
const resolveTypeScript7PackageJson = (rootDir) => {
|
|
834
|
+
for (const candidate of ["typescript/package.json", "typescript-7/package.json"]) {
|
|
835
|
+
const packageJson = resolveOptionalPackage(rootDir, candidate);
|
|
836
|
+
if (packageJson !== void 0 && isTypeScript7Version(readPackageVersion(packageJson))) return {
|
|
837
|
+
ok: true,
|
|
838
|
+
packageJson
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
return failure(/* @__PURE__ */ new Error("Unable to resolve TypeScript 7. Install typescript@rc or typescript-7@npm:typescript@rc in this project."));
|
|
842
|
+
};
|
|
843
|
+
const parseCheckArgs = (args) => {
|
|
844
|
+
let mode = void 0;
|
|
845
|
+
let project = void 0;
|
|
846
|
+
let help = false;
|
|
847
|
+
const tscArgs = [];
|
|
848
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
849
|
+
const arg = args[index];
|
|
850
|
+
if (arg === void 0 || arg === "") continue;
|
|
851
|
+
if (arg === "--help" || arg === "-h") {
|
|
852
|
+
help = true;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (arg === "--mode") {
|
|
856
|
+
const nextArg = args[index + 1];
|
|
857
|
+
if (nextArg === void 0 || nextArg === "") return cliError("--mode requires direct or must-use");
|
|
858
|
+
if (nextArg !== "direct" && nextArg !== "must-use") return cliError(`Unknown --mode value: ${nextArg}`);
|
|
859
|
+
mode = nextArg;
|
|
860
|
+
index += 1;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (arg.startsWith("--mode=")) {
|
|
864
|
+
const nextMode = arg.slice(7);
|
|
865
|
+
if (nextMode !== "direct" && nextMode !== "must-use") return cliError(`Unknown --mode value: ${nextMode}`);
|
|
866
|
+
mode = nextMode;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (arg === "--project" || arg === "-p") {
|
|
870
|
+
const nextArg = args[index + 1];
|
|
871
|
+
if (nextArg === void 0 || nextArg === "") return cliError(`${arg} requires a path`);
|
|
872
|
+
project = nextArg;
|
|
873
|
+
tscArgs.push(arg, nextArg);
|
|
874
|
+
index += 1;
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
if (arg.startsWith("--project=")) project = arg.slice(10);
|
|
878
|
+
tscArgs.push(arg);
|
|
879
|
+
}
|
|
880
|
+
if (project === "") return cliError("--project requires a path");
|
|
881
|
+
if (project === void 0) return {
|
|
882
|
+
ok: true,
|
|
883
|
+
options: mode === void 0 ? {
|
|
884
|
+
help,
|
|
885
|
+
tscArgs
|
|
886
|
+
} : {
|
|
887
|
+
help,
|
|
888
|
+
mode,
|
|
889
|
+
tscArgs
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
return {
|
|
893
|
+
ok: true,
|
|
894
|
+
options: mode === void 0 ? {
|
|
895
|
+
help,
|
|
896
|
+
project,
|
|
897
|
+
tscArgs
|
|
898
|
+
} : {
|
|
899
|
+
help,
|
|
900
|
+
mode,
|
|
901
|
+
project,
|
|
902
|
+
tscArgs
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
};
|
|
906
|
+
const readProject = (tsApi, projectPath) => {
|
|
907
|
+
const formatHost = {
|
|
908
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
909
|
+
getCurrentDirectory: () => process.cwd(),
|
|
910
|
+
getNewLine: () => "\n"
|
|
911
|
+
};
|
|
912
|
+
const config = tsApi.readConfigFile(projectPath, (fileName) => tsApi.sys.readFile(fileName));
|
|
913
|
+
if (config.error) return failure(new Error(tsApi.formatDiagnosticsWithColorAndContext([config.error], formatHost)));
|
|
914
|
+
const parsed = tsApi.parseJsonConfigFileContent(config.config, tsApi.sys, (0, node_path.resolve)(projectPath, ".."), void 0, projectPath);
|
|
915
|
+
if (parsed.errors.length > 0) return failure(new Error(tsApi.formatDiagnosticsWithColorAndContext(parsed.errors, formatHost)));
|
|
916
|
+
return {
|
|
917
|
+
config: config.config,
|
|
918
|
+
ok: true,
|
|
919
|
+
parsed
|
|
920
|
+
};
|
|
921
|
+
};
|
|
922
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
923
|
+
const getProjectRuleOptions = (config) => {
|
|
924
|
+
if (!isRecord(config) || !isRecord(config.compilerOptions)) return;
|
|
925
|
+
const { plugins } = config.compilerOptions;
|
|
926
|
+
if (!Array.isArray(plugins)) return;
|
|
927
|
+
return findResultarPluginConfig(plugins);
|
|
928
|
+
};
|
|
929
|
+
const resolveTypeScriptApi = (_rootDir) => {
|
|
930
|
+
try {
|
|
931
|
+
return {
|
|
932
|
+
ok: true,
|
|
933
|
+
tsApi: requireFromPackage("typescript")
|
|
934
|
+
};
|
|
935
|
+
} catch {
|
|
936
|
+
return failure(/* @__PURE__ */ new Error("Unable to resolve the internal TypeScript diagnostics API bundled with resultar-check."));
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
const findResultarLintFindings = (options = {}) => {
|
|
940
|
+
const rootDir = (0, node_path.resolve)(options.rootDir ?? process.cwd());
|
|
941
|
+
const projectPath = (0, node_path.resolve)(rootDir, options.project ?? "tsconfig.json");
|
|
942
|
+
const resolvedTypeScript = resolveTypeScriptApi(rootDir);
|
|
943
|
+
if (!resolvedTypeScript.ok) return resolvedTypeScript;
|
|
944
|
+
const { tsApi } = resolvedTypeScript;
|
|
945
|
+
const project = readProject(tsApi, projectPath);
|
|
946
|
+
if (!project.ok) return project;
|
|
947
|
+
const projectRuleOptions = getProjectRuleOptions(project.config);
|
|
948
|
+
return success(getProgramResultarFindings(tsApi, tsApi.createProgram(project.parsed.fileNames, project.parsed.options), {
|
|
949
|
+
...projectRuleOptions,
|
|
950
|
+
...options.rules,
|
|
951
|
+
noDiscardMode: normalizeNoDiscardMode(options.mode ?? options.rules?.noDiscardMode ?? projectRuleOptions?.noDiscardMode)
|
|
952
|
+
}));
|
|
953
|
+
};
|
|
954
|
+
const formatFinding = (finding, rootDir) => {
|
|
955
|
+
return [`${(0, node_path.relative)(rootDir, finding.file)}:${finding.line}:${finding.column} resultar/${finding.rule}`, finding.message].join(" - ");
|
|
956
|
+
};
|
|
957
|
+
const passthroughArgs = new Set(["--version", "-v"]);
|
|
958
|
+
const shouldSkipResultarDiagnostics = (args) => args.some((arg) => passthroughArgs.has(arg));
|
|
959
|
+
const runNode = (script, args) => {
|
|
960
|
+
const result = (0, node_child_process.spawnSync)(process.execPath, [script, ...args], { stdio: "inherit" });
|
|
961
|
+
if (result.error !== void 0) throw result.error;
|
|
962
|
+
return result.status ?? 1;
|
|
963
|
+
};
|
|
964
|
+
const runResultarCheckCli = (args = process.argv.slice(2)) => {
|
|
965
|
+
const rootDir = process.cwd();
|
|
966
|
+
const parsedArgs = parseCheckArgs(args);
|
|
967
|
+
if (!parsedArgs.ok) {
|
|
968
|
+
process.stderr.write(`${parsedArgs.error.message}\n`);
|
|
969
|
+
return 1;
|
|
970
|
+
}
|
|
971
|
+
if (parsedArgs.options.help) {
|
|
972
|
+
process.stdout.write(usage);
|
|
973
|
+
return 0;
|
|
974
|
+
}
|
|
975
|
+
const resolvedTypeScript = resolveTypeScript7PackageJson(rootDir);
|
|
976
|
+
if (!resolvedTypeScript.ok) {
|
|
977
|
+
process.stderr.write(`${resolvedTypeScript.error.message}\n`);
|
|
978
|
+
return 1;
|
|
979
|
+
}
|
|
980
|
+
const tscStatus = runNode((0, node_path.join)((0, node_path.dirname)(resolvedTypeScript.packageJson), "bin/tsc"), [...parsedArgs.options.tscArgs]);
|
|
981
|
+
if (tscStatus !== 0 || shouldSkipResultarDiagnostics(args)) return tscStatus;
|
|
982
|
+
const result = parsedArgs.options.project === void 0 ? findResultarLintFindings({
|
|
983
|
+
mode: parsedArgs.options.mode,
|
|
984
|
+
rootDir
|
|
985
|
+
}) : findResultarLintFindings({
|
|
986
|
+
mode: parsedArgs.options.mode,
|
|
987
|
+
project: parsedArgs.options.project,
|
|
988
|
+
rootDir
|
|
989
|
+
});
|
|
990
|
+
if (!result.ok) {
|
|
991
|
+
process.stderr.write(`${result.error.message}\n`);
|
|
992
|
+
return 1;
|
|
993
|
+
}
|
|
994
|
+
if (result.findings.length === 0) return 0;
|
|
995
|
+
process.stderr.write(`${result.findings.map((finding) => formatFinding(finding, rootDir)).join("\n")}\n`);
|
|
996
|
+
return 1;
|
|
997
|
+
};
|
|
998
|
+
//#endregion
|
|
999
|
+
Object.defineProperty(exports, "findResultarLintFindings", {
|
|
1000
|
+
enumerable: true,
|
|
1001
|
+
get: function() {
|
|
1002
|
+
return findResultarLintFindings;
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
Object.defineProperty(exports, "getSourceFileResultarFindings", {
|
|
1006
|
+
enumerable: true,
|
|
1007
|
+
get: function() {
|
|
1008
|
+
return getSourceFileResultarFindings;
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
Object.defineProperty(exports, "normalizeResultarRulesOptions", {
|
|
1012
|
+
enumerable: true,
|
|
1013
|
+
get: function() {
|
|
1014
|
+
return normalizeResultarRulesOptions;
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
Object.defineProperty(exports, "parsePluginOptions", {
|
|
1018
|
+
enumerable: true,
|
|
1019
|
+
get: function() {
|
|
1020
|
+
return parsePluginOptions;
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
Object.defineProperty(exports, "runResultarCheckCli", {
|
|
1024
|
+
enumerable: true,
|
|
1025
|
+
get: function() {
|
|
1026
|
+
return runResultarCheckCli;
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
//# sourceMappingURL=lint-D-D2dmLi.cjs.map
|