resultar 2.0.0 → 3.0.0-alpha.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 +437 -606
- package/dist/index.d.ts +622 -3
- package/dist/index.js +1007 -10
- package/dist/index.js.map +1 -1
- package/package.json +30 -60
- package/dist/no-discard.d.ts +0 -18
- package/dist/no-discard.js +0 -124
- package/dist/no-discard.js.map +0 -1
- package/dist/result-async-CdlVdXgX.js +0 -698
- package/dist/result-async-CdlVdXgX.js.map +0 -1
- package/dist/result-async-CxGBicja.d.ts +0 -439
package/dist/no-discard.js
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { f as err, m as ok } from "./result-async-CdlVdXgX.js";
|
|
3
|
-
import { relative, resolve } from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
import ts from "typescript";
|
|
6
|
-
//#region src/no-discard.ts
|
|
7
|
-
const resultTypeMatcher = /\b(?:DisposableResult|DisposableResultAsync|ErrResult|OkResult|Result|ResultAsync|StrictResult|StrictResultAsync)\b/;
|
|
8
|
-
const formatHost = {
|
|
9
|
-
getCanonicalFileName: (fileName) => fileName,
|
|
10
|
-
getCurrentDirectory: () => process.cwd(),
|
|
11
|
-
getNewLine: () => "\n"
|
|
12
|
-
};
|
|
13
|
-
const usage = `Usage: resultar-no-discard [--project tsconfig.json]
|
|
14
|
-
|
|
15
|
-
Flags:
|
|
16
|
-
-p, --project <path> TypeScript project file to inspect. Defaults to tsconfig.json.
|
|
17
|
-
-h, --help Show this help message.
|
|
18
|
-
`;
|
|
19
|
-
const cliError = (message) => err(new Error(message));
|
|
20
|
-
const parseArgs = (args) => {
|
|
21
|
-
let project = void 0;
|
|
22
|
-
let help = false;
|
|
23
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
24
|
-
const arg = args[index];
|
|
25
|
-
if (arg === "--help" || arg === "-h") help = true;
|
|
26
|
-
else if (arg === "--project" || arg === "-p") {
|
|
27
|
-
const nextArg = args[index + 1];
|
|
28
|
-
if (nextArg === void 0 || nextArg === "") return cliError(`${arg} requires a path`);
|
|
29
|
-
project = nextArg;
|
|
30
|
-
index += 1;
|
|
31
|
-
} else if (arg !== void 0 && arg.startsWith("--project=")) project = arg.slice(10);
|
|
32
|
-
else if (arg !== void 0 && arg !== "") return cliError(`Unknown argument: ${arg}`);
|
|
33
|
-
}
|
|
34
|
-
if (project === "") return cliError("--project requires a path");
|
|
35
|
-
return project === void 0 ? ok({ help }) : ok({
|
|
36
|
-
help,
|
|
37
|
-
project
|
|
38
|
-
});
|
|
39
|
-
};
|
|
40
|
-
const readProject = (projectPath) => {
|
|
41
|
-
const config = ts.readConfigFile(projectPath, (fileName) => ts.sys.readFile(fileName));
|
|
42
|
-
if (config.error) return err(new Error(ts.formatDiagnosticsWithColorAndContext([config.error], formatHost)));
|
|
43
|
-
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, resolve(projectPath, ".."), void 0, projectPath);
|
|
44
|
-
if (parsed.errors.length > 0) return err(new Error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, formatHost)));
|
|
45
|
-
return ok(parsed);
|
|
46
|
-
};
|
|
47
|
-
const isResultLikeType = (checker, node, type) => {
|
|
48
|
-
if (type.isUnionOrIntersection()) return type.types.some((innerType) => isResultLikeType(checker, node, innerType));
|
|
49
|
-
const typeName = checker.typeToString(type, node, ts.TypeFormatFlags.NoTruncation + ts.TypeFormatFlags.UseFullyQualifiedType);
|
|
50
|
-
return resultTypeMatcher.test(typeName);
|
|
51
|
-
};
|
|
52
|
-
const unwrapExpression = (expression) => {
|
|
53
|
-
let current = expression;
|
|
54
|
-
while (ts.isParenthesizedExpression(current)) current = current.expression;
|
|
55
|
-
return current;
|
|
56
|
-
};
|
|
57
|
-
const isExplicitDiscard = (expression) => ts.isVoidExpression(unwrapExpression(expression));
|
|
58
|
-
const isCallLikeDiscard = (expression) => {
|
|
59
|
-
const unwrapped = unwrapExpression(expression);
|
|
60
|
-
if (ts.isAwaitExpression(unwrapped)) return isCallLikeDiscard(unwrapped.expression);
|
|
61
|
-
if (ts.isCallExpression(unwrapped)) return true;
|
|
62
|
-
if (ts.isConditionalExpression(unwrapped)) return isCallLikeDiscard(unwrapped.whenTrue) || isCallLikeDiscard(unwrapped.whenFalse);
|
|
63
|
-
if (ts.isBinaryExpression(unwrapped) && [
|
|
64
|
-
ts.SyntaxKind.AmpersandAmpersandToken,
|
|
65
|
-
ts.SyntaxKind.BarBarToken,
|
|
66
|
-
ts.SyntaxKind.QuestionQuestionToken
|
|
67
|
-
].includes(unwrapped.operatorToken.kind)) return isCallLikeDiscard(unwrapped.right);
|
|
68
|
-
return false;
|
|
69
|
-
};
|
|
70
|
-
const inspectSourceFile = (checker, sourceFile) => {
|
|
71
|
-
const findings = [];
|
|
72
|
-
const visit = (node) => {
|
|
73
|
-
if (ts.isExpressionStatement(node) && !isExplicitDiscard(node.expression) && isCallLikeDiscard(node.expression)) {
|
|
74
|
-
const type = checker.getTypeAtLocation(node.expression);
|
|
75
|
-
if (isResultLikeType(checker, node.expression, type)) {
|
|
76
|
-
const position = sourceFile.getLineAndCharacterOfPosition(node.expression.getStart());
|
|
77
|
-
findings.push({
|
|
78
|
-
column: position.character + 1,
|
|
79
|
-
file: sourceFile.fileName,
|
|
80
|
-
line: position.line + 1,
|
|
81
|
-
type: checker.typeToString(type, node.expression, ts.TypeFormatFlags.NoTruncation)
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
ts.forEachChild(node, visit);
|
|
86
|
-
};
|
|
87
|
-
visit(sourceFile);
|
|
88
|
-
return findings;
|
|
89
|
-
};
|
|
90
|
-
const findDiscardedResults = (options = {}) => {
|
|
91
|
-
return readProject(resolve(resolve(options.rootDir ?? process.cwd()), options.project ?? "tsconfig.json")).map((parsed) => {
|
|
92
|
-
const program = ts.createProgram(parsed.fileNames, parsed.options);
|
|
93
|
-
const checker = program.getTypeChecker();
|
|
94
|
-
const findings = [];
|
|
95
|
-
for (const sourceFile of program.getSourceFiles()) if (!sourceFile.isDeclarationFile && !sourceFile.fileName.includes("/node_modules/")) findings.push(...inspectSourceFile(checker, sourceFile));
|
|
96
|
-
return findings;
|
|
97
|
-
});
|
|
98
|
-
};
|
|
99
|
-
const formatFinding = (finding, rootDir) => {
|
|
100
|
-
return [`${relative(rootDir, finding.file)}:${finding.line}:${finding.column} no-discard-result`, `Ignored ${finding.type} value. Handle it or explicitly discard it with \`void\`.`].join(" - ");
|
|
101
|
-
};
|
|
102
|
-
const runNoDiscardCli = (args = process.argv.slice(2)) => {
|
|
103
|
-
const rootDir = process.cwd();
|
|
104
|
-
return parseArgs(args).andThen((options) => {
|
|
105
|
-
if (!options.help) return options.project === void 0 ? findDiscardedResults({ rootDir }) : findDiscardedResults({
|
|
106
|
-
project: options.project,
|
|
107
|
-
rootDir
|
|
108
|
-
});
|
|
109
|
-
process.stdout.write(usage);
|
|
110
|
-
return ok([]);
|
|
111
|
-
}).match((findings) => {
|
|
112
|
-
if (findings.length === 0) return 0;
|
|
113
|
-
process.stderr.write(`${findings.map((finding) => formatFinding(finding, rootDir)).join("\n")}\n`);
|
|
114
|
-
return 1;
|
|
115
|
-
}, (error) => {
|
|
116
|
-
process.stderr.write(`${error.message}\n`);
|
|
117
|
-
return 1;
|
|
118
|
-
});
|
|
119
|
-
};
|
|
120
|
-
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) process.exitCode = runNoDiscardCli();
|
|
121
|
-
//#endregion
|
|
122
|
-
export { findDiscardedResults, runNoDiscardCli };
|
|
123
|
-
|
|
124
|
-
//# sourceMappingURL=no-discard.js.map
|
package/dist/no-discard.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"no-discard.js","names":[],"sources":["../src/no-discard.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { relative, resolve } from 'node:path'\nimport { pathToFileURL } from 'node:url'\n\nimport ts from 'typescript'\n\nimport type { Result } from './result.js'\n\nimport { err, ok } from './result.js'\n\nconst resultTypeMatcher =\n /\\b(?:DisposableResult|DisposableResultAsync|ErrResult|OkResult|Result|ResultAsync|StrictResult|StrictResultAsync)\\b/\n\nexport interface NoDiscardFinding {\n readonly column: number\n readonly file: string\n readonly line: number\n readonly type: string\n}\n\nexport interface NoDiscardOptions {\n readonly project?: string\n readonly rootDir?: string\n}\n\ninterface CliOptions extends NoDiscardOptions {\n readonly help: boolean\n}\n\nconst formatHost: ts.FormatDiagnosticsHost = {\n getCanonicalFileName: (fileName) => fileName,\n getCurrentDirectory: () => process.cwd(),\n getNewLine: () => '\\n',\n}\n\nconst usage = `Usage: resultar-no-discard [--project tsconfig.json]\n\nFlags:\n -p, --project <path> TypeScript project file to inspect. Defaults to tsconfig.json.\n -h, --help Show this help message.\n`\n\nconst cliError = (message: string): Result<never, Error> => err(new Error(message))\n\nconst parseArgs = (args: readonly string[]): Result<CliOptions, Error> => {\n let project: string | undefined = undefined\n let help = false\n\n for (let index = 0; index < args.length; index += 1) {\n const arg = args[index]\n\n if (arg === '--help' || arg === '-h') {\n help = true\n } else if (arg === '--project' || arg === '-p') {\n const nextArg = args[index + 1]\n\n if (nextArg === undefined || nextArg === '') {\n return cliError(`${arg} requires a path`)\n }\n\n project = nextArg\n index += 1\n } else if (arg !== undefined && arg.startsWith('--project=')) {\n project = arg.slice('--project='.length)\n } else if (arg !== undefined && arg !== '') {\n return cliError(`Unknown argument: ${arg}`)\n }\n }\n\n if (project === '') {\n return cliError('--project requires a path')\n }\n\n return project === undefined ? ok({ help }) : ok({ help, project })\n}\n\nconst readProject = (projectPath: string): Result<ts.ParsedCommandLine, Error> => {\n const config = ts.readConfigFile(projectPath, (fileName) => ts.sys.readFile(fileName))\n\n if (config.error) {\n return err(new Error(ts.formatDiagnosticsWithColorAndContext([config.error], formatHost)))\n }\n\n const parsed = ts.parseJsonConfigFileContent(\n config.config,\n ts.sys,\n resolve(projectPath, '..'),\n undefined,\n projectPath,\n )\n\n if (parsed.errors.length > 0) {\n return err(new Error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, formatHost)))\n }\n\n return ok(parsed)\n}\n\nconst isResultLikeType = (checker: ts.TypeChecker, node: ts.Node, type: ts.Type): boolean => {\n if (type.isUnionOrIntersection()) {\n return type.types.some((innerType) => isResultLikeType(checker, node, innerType))\n }\n\n const typeName = checker.typeToString(\n type,\n node,\n ts.TypeFormatFlags.NoTruncation + ts.TypeFormatFlags.UseFullyQualifiedType,\n )\n\n return resultTypeMatcher.test(typeName)\n}\n\nconst unwrapExpression = (expression: ts.Expression): ts.Expression => {\n let current = expression\n\n while (ts.isParenthesizedExpression(current)) {\n current = current.expression\n }\n\n return current\n}\n\nconst isExplicitDiscard = (expression: ts.Expression): boolean =>\n ts.isVoidExpression(unwrapExpression(expression))\n\nconst isCallLikeDiscard = (expression: ts.Expression): boolean => {\n const unwrapped = unwrapExpression(expression)\n\n if (ts.isAwaitExpression(unwrapped)) {\n return isCallLikeDiscard(unwrapped.expression)\n }\n\n if (ts.isCallExpression(unwrapped)) {\n return true\n }\n\n if (ts.isConditionalExpression(unwrapped)) {\n return isCallLikeDiscard(unwrapped.whenTrue) || isCallLikeDiscard(unwrapped.whenFalse)\n }\n\n if (\n ts.isBinaryExpression(unwrapped) &&\n [\n ts.SyntaxKind.AmpersandAmpersandToken,\n ts.SyntaxKind.BarBarToken,\n ts.SyntaxKind.QuestionQuestionToken,\n ].includes(unwrapped.operatorToken.kind)\n ) {\n return isCallLikeDiscard(unwrapped.right)\n }\n\n return false\n}\n\nconst inspectSourceFile = (\n checker: ts.TypeChecker,\n sourceFile: ts.SourceFile,\n): readonly NoDiscardFinding[] => {\n const findings: NoDiscardFinding[] = []\n\n const visit = (node: ts.Node): void => {\n if (\n ts.isExpressionStatement(node) &&\n !isExplicitDiscard(node.expression) &&\n isCallLikeDiscard(node.expression)\n ) {\n const type = checker.getTypeAtLocation(node.expression)\n\n if (isResultLikeType(checker, node.expression, type)) {\n const position = sourceFile.getLineAndCharacterOfPosition(node.expression.getStart())\n findings.push({\n column: position.character + 1,\n file: sourceFile.fileName,\n line: position.line + 1,\n type: checker.typeToString(type, node.expression, ts.TypeFormatFlags.NoTruncation),\n })\n }\n }\n\n ts.forEachChild(node, visit)\n }\n\n visit(sourceFile)\n\n return findings\n}\n\nexport const findDiscardedResults = (\n options: NoDiscardOptions = {},\n): Result<readonly NoDiscardFinding[], Error> => {\n const rootDir = resolve(options.rootDir ?? process.cwd())\n const projectPath = resolve(rootDir, options.project ?? 'tsconfig.json')\n\n return readProject(projectPath).map((parsed) => {\n const program = ts.createProgram(parsed.fileNames, parsed.options)\n const checker = program.getTypeChecker()\n const findings: NoDiscardFinding[] = []\n\n for (const sourceFile of program.getSourceFiles()) {\n if (!sourceFile.isDeclarationFile && !sourceFile.fileName.includes('/node_modules/')) {\n findings.push(...inspectSourceFile(checker, sourceFile))\n }\n }\n\n return findings\n })\n}\n\nconst formatFinding = (finding: NoDiscardFinding, rootDir: string): string => {\n const file = relative(rootDir, finding.file)\n\n return [\n `${file}:${finding.line}:${finding.column} no-discard-result`,\n `Ignored ${finding.type} value. Handle it or explicitly discard it with \\`void\\`.`,\n ].join(' - ')\n}\n\nexport const runNoDiscardCli = (args: readonly string[] = process.argv.slice(2)): number => {\n const rootDir = process.cwd()\n\n return parseArgs(args)\n .andThen((options) => {\n if (!options.help) {\n return options.project === undefined\n ? findDiscardedResults({ rootDir })\n : findDiscardedResults({ project: options.project, rootDir })\n }\n\n process.stdout.write(usage)\n return ok([] as const)\n })\n .match(\n (findings) => {\n if (findings.length === 0) {\n return 0\n }\n\n process.stderr.write(\n `${findings.map((finding) => formatFinding(finding, rootDir)).join('\\n')}\\n`,\n )\n\n return 1\n },\n (error) => {\n process.stderr.write(`${error.message}\\n`)\n return 1\n },\n )\n}\n\nif (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {\n process.exitCode = runNoDiscardCli()\n}\n"],"mappings":"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"}
|