find-primordials 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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/analyzer.mjs +2209 -0
- package/ignore.mjs +161 -0
- package/index.d.mts +201 -0
- package/index.mjs +381 -0
- package/package.json +85 -0
- package/primordials.d.mts +32 -0
- package/primordials.mjs +836 -0
- package/worker.mjs +35 -0
package/ignore.mjs
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
|
|
2
|
+
import * as minimatchModule from 'minimatch';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the minimatch callable across module shapes (v3 CJS default vs v10+ named export).
|
|
6
|
+
* @param {Record<string, unknown>} mod - The imported minimatch module namespace
|
|
7
|
+
* @returns {(target: string, pattern: string, options?: object) => boolean}
|
|
8
|
+
*/
|
|
9
|
+
export function resolveMinimatch(mod) {
|
|
10
|
+
// @ts-expect-error - interop across minimatch major versions is not statically typed
|
|
11
|
+
return mod.minimatch || mod.default || mod;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Handle both minimatch v3 (CJS default) and v10+ (named export) */
|
|
15
|
+
const minimatch = resolveMinimatch(minimatchModule);
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* Ignore configuration format:
|
|
19
|
+
* {
|
|
20
|
+
* files: ['vendor/**'], // Glob patterns - ignore entire files
|
|
21
|
+
* types: ['spread', 'global'], // Finding types to ignore
|
|
22
|
+
* categories: ['RegExp'], // Categories to ignore
|
|
23
|
+
* names: ['test', 'exec'], // Method/property names to ignore
|
|
24
|
+
* rules: [ // Fine-grained rules
|
|
25
|
+
* { files: ['src/*.js'], types: ['instanceMethod'] },
|
|
26
|
+
* { files: ['helpers/**'], names: ['push'] },
|
|
27
|
+
* ]
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** @import { Finding } from '#/analyzer' */
|
|
32
|
+
|
|
33
|
+
const VALID_TYPES = new Set(/** @type {const} */ ([
|
|
34
|
+
'global',
|
|
35
|
+
'instanceMethod',
|
|
36
|
+
'prototypeAccess',
|
|
37
|
+
'spread',
|
|
38
|
+
'staticMethod',
|
|
39
|
+
'staticProperty',
|
|
40
|
+
]));
|
|
41
|
+
|
|
42
|
+
/** @typedef {{ categories?: string[], files?: string[], names?: string[], rules?: unknown, types?: string[] }} RawIgnoreConfig */
|
|
43
|
+
/** @typedef {{ categories: Set<string>, files: string[], names: Set<string>, rules: { categories: Set<string>, files: string[], names: Set<string>, types: Set<string> }[], types: Set<string> }} IgnoreConfig */
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Normalize ignore config to a consistent format
|
|
47
|
+
* @param {RawIgnoreConfig | IgnoreConfig} config
|
|
48
|
+
* @returns {null | IgnoreConfig}
|
|
49
|
+
*/
|
|
50
|
+
export function normalizeIgnoreConfig(config) {
|
|
51
|
+
if (!config) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
categories: Array.isArray(config.categories) ? new Set(config.categories) : new Set(),
|
|
57
|
+
files: Array.isArray(config.files) ? config.files : [],
|
|
58
|
+
names: Array.isArray(config.names) ? new Set(config.names) : new Set(),
|
|
59
|
+
rules: Array.isArray(config.rules) ? config.rules.map((rule) => ({
|
|
60
|
+
categories: Array.isArray(rule.categories) ? new Set(rule.categories) : new Set(),
|
|
61
|
+
files: Array.isArray(rule.files) ? rule.files : [],
|
|
62
|
+
names: Array.isArray(rule.names) ? new Set(rule.names) : new Set(),
|
|
63
|
+
types: Array.isArray(rule.types) ? new Set(rule.types) : new Set(),
|
|
64
|
+
})) : [],
|
|
65
|
+
types: Array.isArray(config.types) ? new Set(config.types) : new Set(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if a file path matches any of the glob patterns
|
|
71
|
+
* @param {string} filePath
|
|
72
|
+
* @param {string[]} patterns
|
|
73
|
+
*/
|
|
74
|
+
function matchesAnyPattern(filePath, patterns) {
|
|
75
|
+
for (const pattern of patterns) {
|
|
76
|
+
if (minimatch(filePath, pattern, { dot: true, matchBase: true })) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if a file should be ignored entirely
|
|
85
|
+
* @param {string} filePath
|
|
86
|
+
* @param {IgnoreConfig} [ignoreConfig]
|
|
87
|
+
*/
|
|
88
|
+
export function shouldIgnoreFile(filePath, ignoreConfig) {
|
|
89
|
+
if (!ignoreConfig || ignoreConfig.files.length === 0) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return matchesAnyPattern(filePath, ignoreConfig.files);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a finding should be ignored
|
|
97
|
+
* @param {Finding} finding
|
|
98
|
+
* @param {IgnoreConfig} [ignoreConfig]
|
|
99
|
+
*/
|
|
100
|
+
export function shouldIgnoreFinding(finding, ignoreConfig) {
|
|
101
|
+
if (!ignoreConfig) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check global type filter
|
|
106
|
+
if (ignoreConfig.types.has(finding.type)) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check global category filter
|
|
111
|
+
if (finding.category && ignoreConfig.categories.has(finding.category)) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check global name filter
|
|
116
|
+
if (finding.name && ignoreConfig.names.has(finding.name)) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check fine-grained rules
|
|
121
|
+
for (const rule of ignoreConfig.rules) {
|
|
122
|
+
// Rule must match file pattern (if specified) and have at least one filter
|
|
123
|
+
const fileMatches = rule.files.length === 0 || matchesAnyPattern(finding.file, rule.files);
|
|
124
|
+
const hasFilters = rule.types.size > 0 || rule.categories.size > 0 || rule.names.size > 0;
|
|
125
|
+
|
|
126
|
+
if (fileMatches && hasFilters && rule.files.length > 0) {
|
|
127
|
+
// For file-specific rules, ANY matching filter causes ignore
|
|
128
|
+
if (rule.types.has(finding.type)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (finding.category && rule.categories.has(finding.category)) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if (finding.name && rule.names.has(finding.name)) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Filter an array of findings based on ignore config
|
|
145
|
+
* @param {Finding[]} findings
|
|
146
|
+
* @param {IgnoreConfig | null} [ignoreConfig]
|
|
147
|
+
*/
|
|
148
|
+
export function filterFindings(findings, ignoreConfig) {
|
|
149
|
+
if (!ignoreConfig) {
|
|
150
|
+
return findings;
|
|
151
|
+
}
|
|
152
|
+
return findings.filter((finding) => !shouldIgnoreFinding(finding, ignoreConfig));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get valid finding types for documentation/validation
|
|
157
|
+
* @returns {(typeof VALID_TYPES extends Set<infer R> ? R : never)[]}
|
|
158
|
+
*/
|
|
159
|
+
export function getValidTypes() {
|
|
160
|
+
return [...VALID_TYPES];
|
|
161
|
+
}
|
package/index.d.mts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Type declarations for `find-primordials`.
|
|
2
|
+
|
|
3
|
+
/** A source position, as ESTree records it. */
|
|
4
|
+
export type SourceLocation = {
|
|
5
|
+
start: { line: number; column: number };
|
|
6
|
+
end: { line: number; column: number };
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A parsed ESTree/TSESTree node: one permissive shape whose fields the predicates read
|
|
11
|
+
* after checking `type`. A node that may be absent is {@link MaybeNode}.
|
|
12
|
+
*/
|
|
13
|
+
export type ASTNode = {
|
|
14
|
+
type: string;
|
|
15
|
+
name: string;
|
|
16
|
+
operator: string;
|
|
17
|
+
kind: string;
|
|
18
|
+
computed: boolean;
|
|
19
|
+
shorthand: boolean;
|
|
20
|
+
object: ASTNode;
|
|
21
|
+
property: ASTNode;
|
|
22
|
+
callee: ASTNode;
|
|
23
|
+
left: ASTNode;
|
|
24
|
+
right: ASTNode;
|
|
25
|
+
test: ASTNode;
|
|
26
|
+
consequent: ASTNode;
|
|
27
|
+
alternate: ASTNode;
|
|
28
|
+
argument: ASTNode;
|
|
29
|
+
id: ASTNode;
|
|
30
|
+
init: ASTNode;
|
|
31
|
+
key: ASTNode;
|
|
32
|
+
local: ASTNode;
|
|
33
|
+
param: ASTNode;
|
|
34
|
+
arguments: ASTNode[];
|
|
35
|
+
properties: ASTNode[];
|
|
36
|
+
params: ASTNode[];
|
|
37
|
+
expressions: ASTNode[];
|
|
38
|
+
declarations: ASTNode[];
|
|
39
|
+
specifiers: ASTNode[];
|
|
40
|
+
elements: (ASTNode | null)[];
|
|
41
|
+
comments: ASTNode[];
|
|
42
|
+
value: unknown;
|
|
43
|
+
body: unknown;
|
|
44
|
+
range: [number, number];
|
|
45
|
+
loc: SourceLocation;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** An {@link ASTNode} that may be absent. */
|
|
49
|
+
export type MaybeNode = ASTNode | null | undefined;
|
|
50
|
+
|
|
51
|
+
/** The finding kinds that have an autofix. */
|
|
52
|
+
export type FixKind = 'at' | 'constructor' | 'isNaN' | 'push' | 'undefined';
|
|
53
|
+
|
|
54
|
+
/** A finding type accepted in ignore configuration. */
|
|
55
|
+
export type FindingType = 'global' | 'instanceMethod' | 'prototypeAccess' | 'spread' | 'staticMethod' | 'staticProperty';
|
|
56
|
+
|
|
57
|
+
/** A primordial usage the analyzer reports. */
|
|
58
|
+
export type Finding = {
|
|
59
|
+
type: string;
|
|
60
|
+
name: string;
|
|
61
|
+
certainty: string;
|
|
62
|
+
file: string;
|
|
63
|
+
line?: number;
|
|
64
|
+
column?: number;
|
|
65
|
+
category?: string | null;
|
|
66
|
+
possibleCategories?: string[];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** A parse/read error encountered while analyzing a file. */
|
|
70
|
+
export type AnalysisError = {
|
|
71
|
+
error: string;
|
|
72
|
+
file: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** The findings and errors from analyzing one or more files. */
|
|
76
|
+
export type AnalysisResult = {
|
|
77
|
+
errors: AnalysisError[];
|
|
78
|
+
findings: Finding[];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Options accepted by the analyzers. */
|
|
82
|
+
export type AnalyzeOptions = {
|
|
83
|
+
includeGlobals?: boolean;
|
|
84
|
+
includeSpread?: boolean;
|
|
85
|
+
includeStatic?: boolean;
|
|
86
|
+
includeUncertain?: boolean;
|
|
87
|
+
isSafe?: boolean;
|
|
88
|
+
isSafeFile?: ((filePath: string) => boolean) | null;
|
|
89
|
+
concurrency?: number;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** The result of rewriting a file. */
|
|
93
|
+
export type FixResult = {
|
|
94
|
+
fixed: boolean;
|
|
95
|
+
output: string;
|
|
96
|
+
fixCount: number;
|
|
97
|
+
fixCounts: Record<FixKind, number>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Raw ignore configuration, as authored in a config file. */
|
|
101
|
+
export type RawIgnoreConfig = {
|
|
102
|
+
categories?: string[];
|
|
103
|
+
files?: string[];
|
|
104
|
+
names?: string[];
|
|
105
|
+
rules?: unknown;
|
|
106
|
+
types?: string[];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** A fine-grained ignore rule within a normalized {@link IgnoreConfig}. */
|
|
110
|
+
export type IgnoreRule = {
|
|
111
|
+
categories: Set<string>;
|
|
112
|
+
files: string[];
|
|
113
|
+
names: Set<string>;
|
|
114
|
+
types: Set<string>;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** Normalized ignore configuration. */
|
|
118
|
+
export type IgnoreConfig = {
|
|
119
|
+
categories: Set<string>;
|
|
120
|
+
files: string[];
|
|
121
|
+
names: Set<string>;
|
|
122
|
+
rules: IgnoreRule[];
|
|
123
|
+
types: Set<string>;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/** The slice of a TypeScript `Type` that {@link describeType} reads. */
|
|
127
|
+
export type TypeLike = { flags: number };
|
|
128
|
+
|
|
129
|
+
/** The slice of a TypeScript type checker that {@link describeType} reads. */
|
|
130
|
+
export type TypeCheckerLike<T extends TypeLike> = {
|
|
131
|
+
typeToString: (type: T) => string;
|
|
132
|
+
isArrayType?: (type: T) => boolean;
|
|
133
|
+
isTupleType?: (type: T) => boolean;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// ---- analysis ----
|
|
137
|
+
|
|
138
|
+
export function analyzeFile(filePath: string, options?: AnalyzeOptions): { error: string | null; findings: Finding[] };
|
|
139
|
+
export function analyzeFiles(filePaths: string[], options?: AnalyzeOptions): AnalysisResult;
|
|
140
|
+
export function analyzeFilesParallel(filePaths: string[], options?: AnalyzeOptions): Promise<AnalysisResult>;
|
|
141
|
+
|
|
142
|
+
// ---- fixes ----
|
|
143
|
+
|
|
144
|
+
export function applyFixes(filePath: string, findings: Finding[]): FixResult;
|
|
145
|
+
export function applyPushFixes(filePath: string, findings: Finding[]): FixResult;
|
|
146
|
+
export function applyUndefinedFixes(filePath: string, findings: Finding[]): FixResult;
|
|
147
|
+
|
|
148
|
+
// ---- fix predicates (shared with the ESLint plugin's rewrites) ----
|
|
149
|
+
|
|
150
|
+
export function isCalled(node: ASTNode, parent: MaybeNode): boolean;
|
|
151
|
+
export function isRepeatable(node: MaybeNode): boolean;
|
|
152
|
+
export function isReevaluable(node: MaybeNode): boolean;
|
|
153
|
+
export function literalIndex(arg: ASTNode): number | null;
|
|
154
|
+
export function startsAStatement(node: ASTNode, parent: MaybeNode): boolean;
|
|
155
|
+
export function canBeArrayLiteral(args: ASTNode[]): boolean;
|
|
156
|
+
export function voidNeedsParens(node: ASTNode, parent: MaybeNode): boolean;
|
|
157
|
+
export function canRewriteUndefined(parent: MaybeNode): boolean;
|
|
158
|
+
|
|
159
|
+
// ---- type description ----
|
|
160
|
+
|
|
161
|
+
export function describeType<T extends TypeLike>(typeChecker: TypeCheckerLike<T>, type: T): string | null;
|
|
162
|
+
|
|
163
|
+
// ---- formatting / grouping ----
|
|
164
|
+
|
|
165
|
+
export function categoryLabel(finding: Finding): string;
|
|
166
|
+
export function groupFindingsByCategory(findings: Finding[]): Record<string, Finding[]>;
|
|
167
|
+
export function formatFindingAsTAP(finding: Finding, testNum: number): string;
|
|
168
|
+
export function formatAsTAP(findings: Finding[], options?: { showUncertain?: boolean }): string;
|
|
169
|
+
|
|
170
|
+
// ---- ignore configuration ----
|
|
171
|
+
|
|
172
|
+
export function normalizeIgnoreConfig(config: RawIgnoreConfig | IgnoreConfig): IgnoreConfig | null;
|
|
173
|
+
export function shouldIgnoreFile(filePath: string, ignoreConfig?: IgnoreConfig): boolean;
|
|
174
|
+
export function shouldIgnoreFinding(finding: Finding, ignoreConfig?: IgnoreConfig): boolean;
|
|
175
|
+
export function filterFindings(findings: Finding[], ignoreConfig?: IgnoreConfig | null): Finding[];
|
|
176
|
+
export function getValidTypes(): FindingType[];
|
|
177
|
+
|
|
178
|
+
// ---- file classification ----
|
|
179
|
+
|
|
180
|
+
export function isTestFile(filePath: string): boolean;
|
|
181
|
+
export function isConfigFile(filePath: string): boolean;
|
|
182
|
+
export function isBinFile(filePath: string): boolean;
|
|
183
|
+
export function isPrivatePackage(filePath: string): boolean;
|
|
184
|
+
export function isUnpublishedFile(filePath: string): boolean;
|
|
185
|
+
export function isSafeFile(filePath: string): boolean;
|
|
186
|
+
|
|
187
|
+
/** The default file extensions the analyzers scan. */
|
|
188
|
+
export const defaultExtensions: string[];
|
|
189
|
+
|
|
190
|
+
// ---- primordial data (also available at `find-primordials/primordials`) ----
|
|
191
|
+
|
|
192
|
+
export {
|
|
193
|
+
allGlobals,
|
|
194
|
+
allInstanceMethods,
|
|
195
|
+
allStaticMethods,
|
|
196
|
+
ambiguousInstanceMethods,
|
|
197
|
+
globalToCategory,
|
|
198
|
+
primordials,
|
|
199
|
+
typedArrayGlobals,
|
|
200
|
+
} from './primordials.mjs';
|
|
201
|
+
export type { PrimordialCategory } from './primordials.mjs';
|