@safets-org/cli 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.
@@ -0,0 +1,66 @@
1
+ import ts from "typescript";
2
+ export function isNullable(type) {
3
+ if (type.flags & ts.TypeFlags.Undefined) {
4
+ return true;
5
+ }
6
+ if (type.flags & ts.TypeFlags.Null) {
7
+ return true;
8
+ }
9
+ if (type.isUnion()) {
10
+ return type.types.some((innerType) => (innerType.flags & ts.TypeFlags.Undefined) !== 0 ||
11
+ (innerType.flags & ts.TypeFlags.Null) !== 0);
12
+ }
13
+ return false;
14
+ }
15
+ export function isInsideTryCatch(node) {
16
+ let current = node.parent;
17
+ while (current) {
18
+ if (ts.isTryStatement(current)) {
19
+ return true;
20
+ }
21
+ current = current.parent;
22
+ }
23
+ return false;
24
+ }
25
+ export function pos(sf, node) {
26
+ const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart());
27
+ return { line: line + 1, col: character + 1 };
28
+ }
29
+ export function getChainRoot(expr) {
30
+ let current = expr;
31
+ while (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
32
+ current = current.expression;
33
+ }
34
+ return current;
35
+ }
36
+ export function isSubChainDuplicate(node, checker) {
37
+ const parent = node.parent;
38
+ if (!ts.isPropertyAccessExpression(parent)) {
39
+ return false;
40
+ }
41
+ try {
42
+ return isNullable(checker.getTypeAtLocation(parent.expression));
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ export function isOptionalAccess(node) {
49
+ if (node.questionDotToken !== undefined) {
50
+ return true;
51
+ }
52
+ let current = node;
53
+ while (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
54
+ if (ts.isPropertyAccessExpression(current) && current.questionDotToken !== undefined) {
55
+ return true;
56
+ }
57
+ if (ts.isElementAccessExpression(current) && current.questionDotToken !== undefined) {
58
+ return true;
59
+ }
60
+ current = current.expression;
61
+ }
62
+ return false;
63
+ }
64
+ export function hasNonNullAssertion(node) {
65
+ return ts.isNonNullExpression(node.expression);
66
+ }
@@ -0,0 +1,8 @@
1
+ export declare const c: {
2
+ red: (s: string) => string;
3
+ yellow: (s: string) => string;
4
+ green: (s: string) => string;
5
+ cyan: (s: string) => string;
6
+ bold: (s: string) => string;
7
+ dim: (s: string) => string;
8
+ };
@@ -0,0 +1,8 @@
1
+ export const c = {
2
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
3
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
4
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
5
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
6
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
7
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
8
+ };
@@ -0,0 +1,9 @@
1
+ export declare const SKIP_DIRS: Set<string>;
2
+ export declare function isTestFile(filePath: string): boolean;
3
+ export declare function normalizeFilePath(filePath: string): string;
4
+ export declare function toPortablePath(filePath: string): string;
5
+ export declare function toProjectRelativePath(projectRoot: string, filePath: string): string;
6
+ export declare function isAnalyzableTsFile(filePath: string): boolean;
7
+ export declare function isBundleFile(filePath: string): boolean;
8
+ export declare function findTsFiles(dir: string, files?: string[]): string[];
9
+ export declare function findTsConfigFiles(dir: string, includeTests?: boolean, files?: string[]): string[];
@@ -0,0 +1,123 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ export const SKIP_DIRS = new Set([
4
+ "node_modules",
5
+ "dist",
6
+ "build",
7
+ "out",
8
+ ".next",
9
+ ".nuxt",
10
+ "coverage",
11
+ ".storybook",
12
+ ".generated",
13
+ "generated",
14
+ "tmp",
15
+ ".cache",
16
+ "__generated__",
17
+ ".turbo",
18
+ ".vercel",
19
+ ".prisma",
20
+ "evals",
21
+ "integration-tests",
22
+ ]);
23
+ const MAX_FILE_LINES = 5000;
24
+ const TEST_PATTERNS = [
25
+ /\.test\.ts$/,
26
+ /\.spec\.ts$/,
27
+ /\/__tests__\//,
28
+ /\/__tests[^/]*\//,
29
+ /\/test\//,
30
+ /\/tests\//,
31
+ /\/test-utils\//,
32
+ /\/testing\//,
33
+ /\/fixtures\//,
34
+ /\/mocks\//,
35
+ ];
36
+ export function isTestFile(filePath) {
37
+ return TEST_PATTERNS.some((pattern) => pattern.test(filePath.replaceAll("\\", "/")));
38
+ }
39
+ export function normalizeFilePath(filePath) {
40
+ return path.normalize(filePath);
41
+ }
42
+ export function toPortablePath(filePath) {
43
+ return normalizeFilePath(filePath).replaceAll("\\", "/");
44
+ }
45
+ export function toProjectRelativePath(projectRoot, filePath) {
46
+ return toPortablePath(path.relative(projectRoot, normalizeFilePath(filePath)));
47
+ }
48
+ export function isAnalyzableTsFile(filePath) {
49
+ const normalizedPath = normalizeFilePath(filePath);
50
+ const fileName = path.basename(normalizedPath);
51
+ return (fileName.endsWith(".ts") &&
52
+ !fileName.endsWith(".d.ts") &&
53
+ !isBundleFile(normalizedPath));
54
+ }
55
+ export function isBundleFile(filePath) {
56
+ try {
57
+ const content = fs.readFileSync(filePath, "utf-8");
58
+ return content.split("\n").length > MAX_FILE_LINES;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ export function findTsFiles(dir, files = []) {
65
+ try {
66
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
67
+ if (entry.name.startsWith(".")) {
68
+ continue;
69
+ }
70
+ if (SKIP_DIRS.has(entry.name.toLowerCase())) {
71
+ continue;
72
+ }
73
+ const fullPath = path.join(dir, entry.name);
74
+ try {
75
+ if (entry.isDirectory()) {
76
+ findTsFiles(fullPath, files);
77
+ }
78
+ else if (isAnalyzableTsFile(fullPath)) {
79
+ files.push(normalizeFilePath(fullPath));
80
+ }
81
+ }
82
+ catch {
83
+ // Skip unreadable entries.
84
+ }
85
+ }
86
+ }
87
+ catch {
88
+ // Skip unreadable directories.
89
+ }
90
+ return files;
91
+ }
92
+ export function findTsConfigFiles(dir, includeTests = false, files = []) {
93
+ try {
94
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
95
+ if (entry.name.startsWith(".")) {
96
+ continue;
97
+ }
98
+ if (SKIP_DIRS.has(entry.name.toLowerCase())) {
99
+ continue;
100
+ }
101
+ const fullPath = path.join(dir, entry.name);
102
+ const checkPath = entry.isDirectory() ? `${fullPath}${path.sep}` : fullPath;
103
+ if (!includeTests && isTestFile(checkPath)) {
104
+ continue;
105
+ }
106
+ try {
107
+ if (entry.isDirectory()) {
108
+ findTsConfigFiles(fullPath, includeTests, files);
109
+ }
110
+ else if (entry.name === "tsconfig.json") {
111
+ files.push(normalizeFilePath(fullPath));
112
+ }
113
+ }
114
+ catch {
115
+ // Skip unreadable entries.
116
+ }
117
+ }
118
+ }
119
+ catch {
120
+ // Skip unreadable directories.
121
+ }
122
+ return files;
123
+ }
@@ -0,0 +1,45 @@
1
+ export type PatternName = "Unsafe property access" | "Unsafe destructuring" | "Unsafe array index access" | "Unprotected JSON.parse" | "Unsafe process.env access" | "Non-null assertion on nullable" | "Unsafe access after await" | "Unsafe Promise.all destructuring" | "Unsafe Map/Record access";
2
+ export interface CrashReport {
3
+ file: string;
4
+ line: number;
5
+ col: number;
6
+ expr: string;
7
+ rootExpr: string;
8
+ type: string;
9
+ pattern: PatternName;
10
+ confidence: "HIGH" | "MEDIUM";
11
+ crashPath: string[];
12
+ fallback?: boolean;
13
+ }
14
+ export interface BaselineOptions {
15
+ includeTests: boolean;
16
+ }
17
+ export interface BaselineCrash {
18
+ file: string;
19
+ line: number;
20
+ expr: string;
21
+ pattern?: PatternName;
22
+ }
23
+ export interface Baseline {
24
+ version: string;
25
+ date: string;
26
+ options?: BaselineOptions;
27
+ crashes: BaselineCrash[];
28
+ }
29
+ export interface ProgramResult {
30
+ program: import("typescript").Program | null;
31
+ programInputs?: {
32
+ configFile: string | null;
33
+ fileNames: string[];
34
+ options: import("typescript").CompilerOptions;
35
+ rootFileCount: number;
36
+ filteredFileCount: number;
37
+ }[];
38
+ fallback: boolean;
39
+ warnings: string[];
40
+ includeTests: boolean;
41
+ strategy: "root-tsconfig" | "workspace-tsconfigs" | "direct-scan" | "fallback";
42
+ configFiles: string[];
43
+ rootFileCount: number;
44
+ filteredFileCount: number;
45
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@safets-org/cli",
3
+ "version": "1.0.0",
4
+ "description": "Finds common runtime crashes TypeScript can't detect",
5
+ "type": "module",
6
+ "bin": {
7
+ "safets": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.build.json",
11
+ "typecheck": "tsc --noEmit",
12
+ "test": "npm run test:smoke && npm run test:cli && npm run test:detectors && npm run test:integration && npm run test:json && npm run test:action",
13
+ "test:action": "node scripts/test-github-action.mjs",
14
+ "test:cli": "node scripts/test-cli.mjs",
15
+ "test:detectors": "node scripts/test-detectors.mjs",
16
+ "test:integration": "node scripts/test-cli-integration.mjs",
17
+ "test:json": "node scripts/test-json-reporter.mjs",
18
+ "test:smoke": "node ./dist/index.js doctor",
19
+ "validate:real-world": "node scripts/validate-real-world.mjs",
20
+ "pack:check": "node scripts/check-pack.mjs",
21
+ "ci:check": "npm run typecheck && npm test && npm run pack:check",
22
+ "doctor": "node ./dist/index.js doctor",
23
+ "fix": "node ./dist/index.js fix",
24
+ "debt": "node ./dist/index.js debt",
25
+ "baseline": "node ./dist/index.js baseline",
26
+ "prepare": "npm run build"
27
+ },
28
+ "keywords": [
29
+ "typescript",
30
+ "static-analysis",
31
+ "runtime-crashes",
32
+ "linter",
33
+ "safety"
34
+ ],
35
+ "author": "",
36
+ "license": "MIT",
37
+ "devDependencies": {
38
+ "@types/node": "^20.0.0",
39
+ "ts-node": "^10.9.0",
40
+ "typescript": "^5.4.0"
41
+ },
42
+ "peerDependencies": {
43
+ "typescript": ">=4.9.0"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "README.md",
48
+ "LICENSE"
49
+ ]
50
+ }