@chenglou/freerange 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/fr.ts +33 -0
- package/package.json +39 -0
- package/src/analyze.ts +16 -0
- package/src/audit.ts +468 -0
- package/src/domain/number.ts +444 -0
- package/src/domain/value.ts +445 -0
- package/src/engine/analyze.ts +745 -0
- package/src/engine/outcome.ts +137 -0
- package/src/engine/state.ts +155 -0
- package/src/engine/transfer.ts +1723 -0
- package/src/index.ts +23 -0
- package/src/ir/finite-inputs.ts +51 -0
- package/src/ir/function-usage.ts +50 -0
- package/src/ir/ids.ts +10 -0
- package/src/ir/instructions.ts +164 -0
- package/src/ir/program.ts +446 -0
- package/src/lower/accept.ts +119 -0
- package/src/lower/context.ts +250 -0
- package/src/lower/expression.ts +1742 -0
- package/src/lower/literals.ts +84 -0
- package/src/lower/module.ts +748 -0
- package/src/lower/platform.ts +81 -0
- package/src/lower/program.ts +310 -0
- package/src/lower/statements.ts +553 -0
- package/src/lower/static-intrinsics.ts +87 -0
- package/src/project.ts +500 -0
- package/src/report/format-requirement.ts +110 -0
- package/src/report/index.ts +1132 -0
- package/src/requirements/infer.ts +334 -0
- package/src/requirements/model.ts +77 -0
- package/src/typescript/check.ts +56 -0
- package/src/typescript/diagnostics.ts +69 -0
- package/src/typescript/project.ts +101 -0
package/src/audit.ts
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import type {AssertionVerdict, ProgramAnalysis, Stop, StopReason} from './engine/outcome.ts'
|
|
2
|
+
import type {SiteID} from './ir/ids.ts'
|
|
3
|
+
import {
|
|
4
|
+
reportPath,
|
|
5
|
+
siteLocation,
|
|
6
|
+
type ProgramIR,
|
|
7
|
+
type UnsupportedReason,
|
|
8
|
+
} from './ir/program.ts'
|
|
9
|
+
import type {
|
|
10
|
+
BoundsAssumption,
|
|
11
|
+
InferredPrecondition,
|
|
12
|
+
NumericExpression,
|
|
13
|
+
} from './requirements/model.ts'
|
|
14
|
+
import {createReport, formatReport, type AnalysisReport} from './report/index.ts'
|
|
15
|
+
import {color, formatDiagnosticPrefix} from './typescript/diagnostics.ts'
|
|
16
|
+
|
|
17
|
+
type RefactorGuideShape = {
|
|
18
|
+
id: string
|
|
19
|
+
title: string
|
|
20
|
+
summary: string
|
|
21
|
+
caveat: string
|
|
22
|
+
before: string
|
|
23
|
+
after: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const refactorGuides = [
|
|
27
|
+
{
|
|
28
|
+
id: 'guard-derived-value',
|
|
29
|
+
title: 'Check the exact divisor',
|
|
30
|
+
summary: 'Give the divisor expression a name, then handle zero before dividing.',
|
|
31
|
+
caveat: 'The function owns the zero case. If zero is invalid input, keep the caller requirement instead.',
|
|
32
|
+
before: `export function remap(value: number, oldMin: number, oldMax: number, newMin: number, newMax: number): number {
|
|
33
|
+
if (oldMin === oldMax) return (newMin + newMax) / 2
|
|
34
|
+
return (value - oldMin) / (oldMax - oldMin) * (newMax - newMin) + newMin
|
|
35
|
+
}`,
|
|
36
|
+
after: `export function remap(value: number, oldMin: number, oldMax: number, newMin: number, newMax: number): number {
|
|
37
|
+
const oldSpan = oldMax - oldMin
|
|
38
|
+
if (oldMin === oldMax) return (newMin + newMax) / 2
|
|
39
|
+
if (oldSpan === 0) return (newMin + newMax) / 2
|
|
40
|
+
return (value - oldMin) / oldSpan * (newMax - newMin) + newMin
|
|
41
|
+
}`,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'encode-input-rule',
|
|
45
|
+
title: 'Encode a real input rule where the calculation begins',
|
|
46
|
+
summary: 'Turn a domain rule such as "column count is a positive integer" into code once, before downstream calculations use it.',
|
|
47
|
+
caveat: 'A positive integer is the real API rule. This changes fractional and nonpositive values, and external NaN still needs validation.',
|
|
48
|
+
before: `export function perColumn(total: number, columnCount: number): number {
|
|
49
|
+
if (columnCount === 0) return 0
|
|
50
|
+
return total / columnCount
|
|
51
|
+
}`,
|
|
52
|
+
after: `export function perColumn(total: number, columnCount: number): number {
|
|
53
|
+
const columns = Math.max(1, Math.floor(columnCount))
|
|
54
|
+
return total / columns
|
|
55
|
+
}`,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'use-direct-operands',
|
|
59
|
+
title: 'Use guarded dimensions directly instead of dividing by a ratio',
|
|
60
|
+
summary: 'A positive ratio can still round down to zero. Guard the original dimensions and divide by one of those values directly.',
|
|
61
|
+
caveat: 'The positive minimum is a real product rule and small rounding differences are acceptable. Nonpositive values change, NaN still needs validation, and multiplication can still overflow.',
|
|
62
|
+
before: `export function fittedHeight(frameWidth: number, imageWidth: number, imageHeight: number): number {
|
|
63
|
+
const aspectRatio = imageWidth / imageHeight
|
|
64
|
+
return frameWidth / aspectRatio
|
|
65
|
+
}`,
|
|
66
|
+
after: `export function fittedHeight(frameWidth: number, imageWidth: number, imageHeight: number): number {
|
|
67
|
+
const width = Math.max(1, imageWidth)
|
|
68
|
+
const height = Math.max(1, imageHeight)
|
|
69
|
+
return (frameWidth * height) / width
|
|
70
|
+
}`,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'write-explicit-condition',
|
|
74
|
+
title: 'Write the numeric case explicitly',
|
|
75
|
+
summary: 'Replace number truthiness with the exact comparison that expresses the intended case.',
|
|
76
|
+
caveat: 'The code means a specific condition such as zero. Choose the comparison that states that condition; NaN can behave differently from truthiness.',
|
|
77
|
+
before: `export function safeWidth(width: number): number {
|
|
78
|
+
return width || 1
|
|
79
|
+
}`,
|
|
80
|
+
after: `export function safeWidth(width: number): number {
|
|
81
|
+
return width === 0 ? 1 : width
|
|
82
|
+
}`,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'use-loop-for-aggregation',
|
|
86
|
+
title: 'Use an explicit loop for dense-array aggregation',
|
|
87
|
+
summary: 'For a simple aggregation, a for loop exposes the accumulator and each numeric step.',
|
|
88
|
+
caveat: 'The array is dense, the reduction has an initial value, and callback arguments or effects do not matter. Indexed loops differ for sparse arrays.',
|
|
89
|
+
before: `export function total(values: number[]): number {
|
|
90
|
+
return values.reduce((sum, value) => sum + value, 0)
|
|
91
|
+
}`,
|
|
92
|
+
after: `export function total(values: number[]): number {
|
|
93
|
+
let sum = 0
|
|
94
|
+
for (let index = 0; index < values.length; index++) {
|
|
95
|
+
sum += values[index]!
|
|
96
|
+
}
|
|
97
|
+
return sum
|
|
98
|
+
}`,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'handle-missing-element',
|
|
102
|
+
title: 'Handle a possibly missing array element',
|
|
103
|
+
summary: 'A bare array read may be undefined even when the project\'s TypeScript settings show a plain element type. Handle the missing case before using the value.',
|
|
104
|
+
caveat: 'A fallback is real application behavior. Otherwise validate or throw; a bounds check alone does not detect a sparse-array hole.',
|
|
105
|
+
before: `export function incrementAt(values: number[], index: number): number {
|
|
106
|
+
return values[index] + 1
|
|
107
|
+
}`,
|
|
108
|
+
after: `export function incrementAt(values: number[], index: number): number {
|
|
109
|
+
const value = values[index] ?? 0
|
|
110
|
+
return value + 1
|
|
111
|
+
}`,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'guard-array-index',
|
|
115
|
+
title: 'Check an asserted array index',
|
|
116
|
+
summary: 'Before using values[index]!, prove that the index is an integer inside the array bounds.',
|
|
117
|
+
caveat: 'The function owns invalid-index behavior. Otherwise keep the caller requirement; any fallback changes an invalid read.',
|
|
118
|
+
before: `export function valueAt(values: number[], index: number): number {
|
|
119
|
+
return values[index]!
|
|
120
|
+
}`,
|
|
121
|
+
after: `export function valueAt(values: number[], index: number): number {
|
|
122
|
+
if (!Number.isInteger(index) || index < 0 || index >= values.length) return 0
|
|
123
|
+
return values[index]!
|
|
124
|
+
}`,
|
|
125
|
+
},
|
|
126
|
+
] as const satisfies readonly RefactorGuideShape[]
|
|
127
|
+
|
|
128
|
+
export type RefactorGuide = (typeof refactorGuides)[number]
|
|
129
|
+
export type RefactorGuideID = RefactorGuide['id']
|
|
130
|
+
|
|
131
|
+
export type AuditCoverage = {
|
|
132
|
+
functions: number
|
|
133
|
+
analyzed: number
|
|
134
|
+
partial: number
|
|
135
|
+
unsupported: number
|
|
136
|
+
initializer: 'analyzed' | 'partial'
|
|
137
|
+
initializerSkips: number
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type AuditReason =
|
|
141
|
+
| {kind: 'requires'; precondition: InferredPrecondition}
|
|
142
|
+
| {kind: 'assumes'; assumption: BoundsAssumption}
|
|
143
|
+
| {kind: 'assertion'; assertion: AssertionVerdict}
|
|
144
|
+
| {kind: 'staticAnnotationIssue'; issue: ProgramIR['staticAnnotationIssues'][number]}
|
|
145
|
+
| {kind: 'unsupported'; reason: UnsupportedReason}
|
|
146
|
+
| {kind: 'partialSupport'; reason: StopReason}
|
|
147
|
+
| {kind: 'skipped'; reason: UnsupportedReason}
|
|
148
|
+
|
|
149
|
+
export type AuditReference = {
|
|
150
|
+
functionName: string
|
|
151
|
+
line: number
|
|
152
|
+
column: number
|
|
153
|
+
span: {start: number; end: number}
|
|
154
|
+
reason: AuditReason
|
|
155
|
+
guideIDs: RefactorGuideID[]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type FileAudit = {
|
|
159
|
+
file: string
|
|
160
|
+
coverage: AuditCoverage
|
|
161
|
+
contracts: AnalysisReport
|
|
162
|
+
references: AuditReference[]
|
|
163
|
+
guideIDs: RefactorGuideID[]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function createFileAudit({program, analysis}: {program: ProgramIR; analysis: ProgramAnalysis}): FileAudit {
|
|
167
|
+
const contracts = createReport(program, analysis)
|
|
168
|
+
let analyzed = 0
|
|
169
|
+
let partial = 0
|
|
170
|
+
let unsupported = 0
|
|
171
|
+
const references: AuditReference[] = []
|
|
172
|
+
|
|
173
|
+
const addReference = (
|
|
174
|
+
functionName: string,
|
|
175
|
+
site: SiteID,
|
|
176
|
+
reason: AuditReason,
|
|
177
|
+
): void => {
|
|
178
|
+
const span = program.sites[site]
|
|
179
|
+
if (span == null) throw new Error(`Unknown site ${site}`)
|
|
180
|
+
references.push({
|
|
181
|
+
functionName,
|
|
182
|
+
...siteLocation(program, site),
|
|
183
|
+
span: {...span},
|
|
184
|
+
reason,
|
|
185
|
+
guideIDs: guidesForReason(reason),
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const addPartialReason = (functionName: string, stop: Stop): void => {
|
|
190
|
+
addReference(functionName, stop.site, {kind: 'partialSupport', reason: stop.reason})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const addPrecondition = (functionName: string, precondition: InferredPrecondition): void => {
|
|
194
|
+
addReference(functionName, precondition.site, {kind: 'requires', precondition})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const addAssumption = (functionName: string, assumption: BoundsAssumption): void => {
|
|
198
|
+
addReference(functionName, assumption.site, {kind: 'assumes', assumption})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const addAssertion = (functionName: string, assertion: AssertionVerdict): void => {
|
|
202
|
+
addReference(functionName, assertion.site, {kind: 'assertion', assertion})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const fn of analysis.functions) {
|
|
206
|
+
switch (fn.kind) {
|
|
207
|
+
case 'analyzed': {
|
|
208
|
+
analyzed++
|
|
209
|
+
for (const precondition of fn.preconditions) addPrecondition(fn.lowering.name, precondition)
|
|
210
|
+
for (const assumption of fn.boundsAssumptions) addAssumption(fn.lowering.name, assumption)
|
|
211
|
+
for (const assertion of fn.assertions) addAssertion(fn.lowering.name, assertion)
|
|
212
|
+
break
|
|
213
|
+
}
|
|
214
|
+
case 'partial': {
|
|
215
|
+
partial++
|
|
216
|
+
for (const precondition of fn.observedNeeds) addPrecondition(fn.lowering.name, precondition)
|
|
217
|
+
for (const assumption of fn.observedBoundsAssumptions) addAssumption(fn.lowering.name, assumption)
|
|
218
|
+
for (const assertion of fn.assertions) addAssertion(fn.lowering.name, assertion)
|
|
219
|
+
for (const stop of fn.stops) addPartialReason(fn.lowering.name, stop)
|
|
220
|
+
break
|
|
221
|
+
}
|
|
222
|
+
case 'notLowered': {
|
|
223
|
+
unsupported++
|
|
224
|
+
addReference(
|
|
225
|
+
fn.lowering.name,
|
|
226
|
+
fn.lowering.site,
|
|
227
|
+
{kind: 'unsupported', reason: fn.lowering.reason},
|
|
228
|
+
)
|
|
229
|
+
break
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (analysis.initializer.kind === 'partial') {
|
|
235
|
+
for (const precondition of analysis.initializer.observedNeeds) {
|
|
236
|
+
addPrecondition(program.initializer.name, precondition)
|
|
237
|
+
}
|
|
238
|
+
for (const assumption of analysis.initializer.observedBoundsAssumptions) {
|
|
239
|
+
addAssumption(program.initializer.name, assumption)
|
|
240
|
+
}
|
|
241
|
+
for (const stop of analysis.initializer.stops) addPartialReason(program.initializer.name, stop)
|
|
242
|
+
}
|
|
243
|
+
for (const assertion of analysis.initializer.assertions) {
|
|
244
|
+
addAssertion(program.initializer.name, assertion)
|
|
245
|
+
}
|
|
246
|
+
for (const issue of program.staticAnnotationIssues) {
|
|
247
|
+
addReference(
|
|
248
|
+
program.initializer.name,
|
|
249
|
+
issue.site,
|
|
250
|
+
{kind: 'staticAnnotationIssue', issue},
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
for (const skip of program.initializerSkips) {
|
|
254
|
+
addReference(
|
|
255
|
+
program.initializer.name,
|
|
256
|
+
skip.site,
|
|
257
|
+
{kind: 'skipped', reason: skip.reason},
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const initializer = analysis.initializer.kind
|
|
262
|
+
const functions = analysis.functions.length
|
|
263
|
+
const initializerSkips = program.initializerSkips.length
|
|
264
|
+
references.sort((left, right) => left.span.start - right.span.start || left.span.end - right.span.end)
|
|
265
|
+
const guideIDs: RefactorGuideID[] = []
|
|
266
|
+
for (const reference of references) {
|
|
267
|
+
for (const guideID of reference.guideIDs) {
|
|
268
|
+
if (!guideIDs.includes(guideID)) guideIDs.push(guideID)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
file: reportPath(program),
|
|
273
|
+
coverage: {
|
|
274
|
+
functions,
|
|
275
|
+
analyzed,
|
|
276
|
+
partial,
|
|
277
|
+
unsupported,
|
|
278
|
+
initializer,
|
|
279
|
+
initializerSkips,
|
|
280
|
+
},
|
|
281
|
+
contracts,
|
|
282
|
+
references,
|
|
283
|
+
guideIDs,
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function formatAuditCoverage(coverage: AuditCoverage): string {
|
|
288
|
+
const parts = coverage.functions === 0
|
|
289
|
+
? ['no named function declarations']
|
|
290
|
+
: [`${coverage.analyzed}/${coverage.functions} functions fully analyzed`]
|
|
291
|
+
if (coverage.partial > 0) parts.push(`${coverage.partial} partially supported`)
|
|
292
|
+
if (coverage.unsupported > 0) parts.push(`${coverage.unsupported} unsupported`)
|
|
293
|
+
if (coverage.initializer !== 'analyzed') parts.push('module setup partially supported')
|
|
294
|
+
if (coverage.initializerSkips > 0) {
|
|
295
|
+
parts.push(`${coverage.initializerSkips} module statement${coverage.initializerSkips === 1 ? '' : 's'} skipped`)
|
|
296
|
+
}
|
|
297
|
+
return parts.join('; ')
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// One file's audit unit: a header carrying the file's coverage counts, its contract
|
|
301
|
+
// entries, then its refactoring suggestions — always in that order, under these exact
|
|
302
|
+
// section labels. `fr --audit <file>` prints one unit; bare `fr --audit` prints one unit
|
|
303
|
+
// per project file, so a file's unit stays identical between the two outputs.
|
|
304
|
+
export function formatFileAuditUnit(audit: FileAudit, pretty = false): string {
|
|
305
|
+
const {coverage} = audit
|
|
306
|
+
const file = pretty ? color(96, audit.file) : audit.file
|
|
307
|
+
const lines = [`# ${file} (${formatAuditCoverage(coverage)})`]
|
|
308
|
+
if (audit.contracts.functions.length > 0) {
|
|
309
|
+
lines.push(
|
|
310
|
+
'',
|
|
311
|
+
'## Contracts',
|
|
312
|
+
'',
|
|
313
|
+
colorAuditLocations(formatReport(audit.contracts), audit.file, pretty),
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (audit.guideIDs.length > 0) {
|
|
318
|
+
const printedSuggestions = new Set<string>()
|
|
319
|
+
lines.push(
|
|
320
|
+
'',
|
|
321
|
+
'## Refactoring suggestions',
|
|
322
|
+
)
|
|
323
|
+
for (const reference of audit.references) {
|
|
324
|
+
const guideIDs = reference.guideIDs.filter(guideID =>
|
|
325
|
+
!printedSuggestions.has(`${reference.span.start}:${guideID}`))
|
|
326
|
+
if (guideIDs.length === 0) continue
|
|
327
|
+
lines.push('')
|
|
328
|
+
for (const guideID of guideIDs) {
|
|
329
|
+
printedSuggestions.add(`${reference.span.start}:${guideID}`)
|
|
330
|
+
const guide = refactorGuide(guideID)
|
|
331
|
+
const prefix = formatDiagnosticPrefix(
|
|
332
|
+
{file: audit.file, line: reference.line, column: reference.column},
|
|
333
|
+
'suggestion',
|
|
334
|
+
guide.id,
|
|
335
|
+
pretty,
|
|
336
|
+
)
|
|
337
|
+
lines.push(`${prefix}${guide.title}. ${guide.summary}`)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return lines.join('\n')
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function colorAuditLocations(output: string, file: string, pretty: boolean): string {
|
|
345
|
+
if (!pretty) return output
|
|
346
|
+
const escapedFile = file.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
347
|
+
return output.replace(
|
|
348
|
+
new RegExp(`${escapedFile}:(\\d+):(\\d+)`, 'g'),
|
|
349
|
+
(_location, line: string, column: string) =>
|
|
350
|
+
`${color(96, file)}:${color(93, line)}:${color(93, column)}`,
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function refactorGuide(id: RefactorGuideID): RefactorGuide {
|
|
355
|
+
const guide = refactorGuides.find(candidate => candidate.id === id)
|
|
356
|
+
if (guide == null) throw new Error(`Missing refactor guide ${id}`)
|
|
357
|
+
return guide
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function guidesForReason(reason: AuditReason): RefactorGuideID[] {
|
|
361
|
+
switch (reason.kind) {
|
|
362
|
+
case 'requires': return guidesForPrecondition(reason.precondition)
|
|
363
|
+
case 'assumes': return reason.assumption.kind === 'nonzeroDivisor'
|
|
364
|
+
? ['guard-derived-value']
|
|
365
|
+
: ['guard-array-index']
|
|
366
|
+
case 'assertion':
|
|
367
|
+
case 'staticAnnotationIssue': return []
|
|
368
|
+
case 'unsupported': return guidesForUnsupportedReason(reason.reason)
|
|
369
|
+
case 'partialSupport': return guidesForStop(reason.reason)
|
|
370
|
+
case 'skipped': return guidesForUnsupportedReason(reason.reason)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function guidesForPrecondition(precondition: InferredPrecondition): RefactorGuideID[] {
|
|
375
|
+
switch (precondition.kind) {
|
|
376
|
+
case 'inBounds': return ['guard-array-index']
|
|
377
|
+
case 'declaredComparison':
|
|
378
|
+
case 'declaredNumberCheck': return []
|
|
379
|
+
case 'nonzero':
|
|
380
|
+
case 'notEqualConstant': break
|
|
381
|
+
}
|
|
382
|
+
const directRatio = precondition.kind === 'nonzero'
|
|
383
|
+
&& precondition.operation === 'division'
|
|
384
|
+
&& precondition.expression.kind === 'binary'
|
|
385
|
+
&& precondition.expression.operator === 'divide'
|
|
386
|
+
const guides: RefactorGuideID[] = directRatio
|
|
387
|
+
? ['use-direct-operands', 'guard-derived-value']
|
|
388
|
+
: ['guard-derived-value']
|
|
389
|
+
if (precondition.kind === 'nonzero') {
|
|
390
|
+
if (isCallerInput(precondition.expression)) guides.push('encode-input-rule')
|
|
391
|
+
}
|
|
392
|
+
return guides
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function guidesForUnsupportedReason(reason: UnsupportedReason): RefactorGuideID[] {
|
|
396
|
+
// A rejection selects a guide only when its structured fields identify the relevant
|
|
397
|
+
// rewrite shape. Generic calls, mutation, and display text are not enough evidence.
|
|
398
|
+
switch (reason.kind) {
|
|
399
|
+
case 'call': return reason.arrayMethod === 'reduce'
|
|
400
|
+
? ['use-loop-for-aggregation']
|
|
401
|
+
: []
|
|
402
|
+
case 'nonBooleanCondition': return reason.conditionKind === 'number'
|
|
403
|
+
? ['write-explicit-condition']
|
|
404
|
+
: []
|
|
405
|
+
case 'unknownIdentifier':
|
|
406
|
+
case 'missingSymbol':
|
|
407
|
+
case 'functionWithoutSignature':
|
|
408
|
+
case 'functionWithoutBody':
|
|
409
|
+
case 'destructuredParameter':
|
|
410
|
+
case 'parameterType':
|
|
411
|
+
case 'parameterDefaultValue':
|
|
412
|
+
case 'missingReturn':
|
|
413
|
+
case 'objectPropertyForm':
|
|
414
|
+
case 'computedPropertyName':
|
|
415
|
+
case 'objectSpread':
|
|
416
|
+
case 'asyncOrGeneratorFunction':
|
|
417
|
+
case 'typePredicate':
|
|
418
|
+
case 'protoProperty':
|
|
419
|
+
case 'enumMemberRead':
|
|
420
|
+
case 'prototypeMemberRead':
|
|
421
|
+
case 'binaryOperator':
|
|
422
|
+
case 'callWithFewerArguments':
|
|
423
|
+
case 'callWithMoreArguments':
|
|
424
|
+
case 'nonNumberOperand':
|
|
425
|
+
case 'valueType':
|
|
426
|
+
case 'kindChangingAssertion':
|
|
427
|
+
case 'propertyReadOnNonObject':
|
|
428
|
+
case 'statementAfterReturn':
|
|
429
|
+
case 'assignmentInValuePosition':
|
|
430
|
+
case 'propertyWrite':
|
|
431
|
+
case 'staticAssertionForm':
|
|
432
|
+
case 'varDeclaration':
|
|
433
|
+
case 'evalInFile':
|
|
434
|
+
case 'typeCheckSuppressed':
|
|
435
|
+
case 'forLoopWithoutCondition':
|
|
436
|
+
case 'variableDeclarationShape':
|
|
437
|
+
case 'expressionForm':
|
|
438
|
+
case 'statementForm':
|
|
439
|
+
case 'switchFallthrough':
|
|
440
|
+
case 'switchDefaultNotLast':
|
|
441
|
+
case 'switchSubject':
|
|
442
|
+
case 'switchLabel': return []
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function guidesForStop(reason: StopReason): RefactorGuideID[] {
|
|
447
|
+
switch (reason.kind) {
|
|
448
|
+
case 'unsupportedCode': return guidesForUnsupportedReason(reason.reason)
|
|
449
|
+
case 'possiblyMissingElement': return ['handle-missing-element']
|
|
450
|
+
case 'requirementFailure':
|
|
451
|
+
case 'moduleRead':
|
|
452
|
+
case 'recursion':
|
|
453
|
+
case 'calleeStopped':
|
|
454
|
+
case 'loopLimit':
|
|
455
|
+
case 'nonExitingLoop':
|
|
456
|
+
case 'kindMismatch': return []
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function isCallerInput(expression: NumericExpression): boolean {
|
|
461
|
+
switch (expression.kind) {
|
|
462
|
+
case 'parameter': return true
|
|
463
|
+
case 'property': return isCallerInput(expression.base)
|
|
464
|
+
case 'floor': return isCallerInput(expression.operand)
|
|
465
|
+
case 'constant':
|
|
466
|
+
case 'binary': return false
|
|
467
|
+
}
|
|
468
|
+
}
|