@openrewrite/recipes-nodejs 0.52.0-20261009-102010 → 0.52.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.
Files changed (60) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +2 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/migrate/code-style.d.ts +14 -0
  5. package/dist/migrate/code-style.d.ts.map +1 -0
  6. package/dist/migrate/code-style.js +70 -0
  7. package/dist/migrate/code-style.js.map +1 -0
  8. package/dist/migrate/hpagent/find-hpagent-manual-migrations.d.ts.map +1 -1
  9. package/dist/migrate/hpagent/find-hpagent-manual-migrations.js +36 -133
  10. package/dist/migrate/hpagent/find-hpagent-manual-migrations.js.map +1 -1
  11. package/dist/migrate/hpagent/hpagent-support.d.ts +36 -15
  12. package/dist/migrate/hpagent/hpagent-support.d.ts.map +1 -1
  13. package/dist/migrate/hpagent/hpagent-support.js +294 -55
  14. package/dist/migrate/hpagent/hpagent-support.js.map +1 -1
  15. package/dist/migrate/hpagent/replace-hpagent.d.ts.map +1 -1
  16. package/dist/migrate/hpagent/replace-hpagent.js +510 -153
  17. package/dist/migrate/hpagent/replace-hpagent.js.map +1 -1
  18. package/dist/migrate/source-map-support/enable-source-maps-at-launch.d.ts.map +1 -1
  19. package/dist/migrate/source-map-support/enable-source-maps-at-launch.js +8 -16
  20. package/dist/migrate/source-map-support/enable-source-maps-at-launch.js.map +1 -1
  21. package/dist/migrate/source-map-support/find-source-map-support-manual-migrations.d.ts.map +1 -1
  22. package/dist/migrate/source-map-support/find-source-map-support-manual-migrations.js +26 -9
  23. package/dist/migrate/source-map-support/find-source-map-support-manual-migrations.js.map +1 -1
  24. package/dist/migrate/source-map-support/migrate-source-map-support-to-native.d.ts.map +1 -1
  25. package/dist/migrate/source-map-support/migrate-source-map-support-to-native.js +7 -4
  26. package/dist/migrate/source-map-support/migrate-source-map-support-to-native.js.map +1 -1
  27. package/dist/migrate/source-map-support/replace-source-map-support-api.d.ts +9 -0
  28. package/dist/migrate/source-map-support/replace-source-map-support-api.d.ts.map +1 -0
  29. package/dist/migrate/source-map-support/replace-source-map-support-api.js +275 -0
  30. package/dist/migrate/source-map-support/replace-source-map-support-api.js.map +1 -0
  31. package/dist/migrate/source-map-support/replace-source-map-support-install.d.ts.map +1 -1
  32. package/dist/migrate/source-map-support/replace-source-map-support-install.js +2 -1
  33. package/dist/migrate/source-map-support/replace-source-map-support-install.js.map +1 -1
  34. package/dist/migrate/source-map-support/source-map-support-api-source.d.ts +14 -0
  35. package/dist/migrate/source-map-support/source-map-support-api-source.d.ts.map +1 -0
  36. package/dist/migrate/source-map-support/source-map-support-api-source.js +426 -0
  37. package/dist/migrate/source-map-support/source-map-support-api-source.js.map +1 -0
  38. package/dist/migrate/source-map-support/source-map-support-rewrites.d.ts.map +1 -1
  39. package/dist/migrate/source-map-support/source-map-support-rewrites.js +2 -0
  40. package/dist/migrate/source-map-support/source-map-support-rewrites.js.map +1 -1
  41. package/dist/migrate/source-map-support/source-map-support-support.d.ts +11 -3
  42. package/dist/migrate/source-map-support/source-map-support-support.d.ts.map +1 -1
  43. package/dist/migrate/source-map-support/source-map-support-support.js +69 -10
  44. package/dist/migrate/source-map-support/source-map-support-support.js.map +1 -1
  45. package/dist/resources/advisories-npm.csv +342 -6
  46. package/package.json +2 -2
  47. package/src/index.ts +2 -0
  48. package/src/migrate/code-style.ts +92 -0
  49. package/src/migrate/fs-extra/replace-fs-extra-native-methods.ts +2 -2
  50. package/src/migrate/hpagent/find-hpagent-manual-migrations.ts +45 -142
  51. package/src/migrate/hpagent/hpagent-support.ts +358 -60
  52. package/src/migrate/hpagent/replace-hpagent.ts +580 -189
  53. package/src/migrate/source-map-support/enable-source-maps-at-launch.ts +17 -26
  54. package/src/migrate/source-map-support/find-source-map-support-manual-migrations.ts +25 -9
  55. package/src/migrate/source-map-support/migrate-source-map-support-to-native.ts +7 -4
  56. package/src/migrate/source-map-support/replace-source-map-support-api.ts +281 -0
  57. package/src/migrate/source-map-support/replace-source-map-support-install.ts +2 -1
  58. package/src/migrate/source-map-support/source-map-support-api-source.ts +470 -0
  59. package/src/migrate/source-map-support/source-map-support-rewrites.ts +3 -0
  60. package/src/migrate/source-map-support/source-map-support-support.ts +79 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openrewrite/recipes-nodejs",
3
- "version": "0.52.0-20261009-102010",
3
+ "version": "0.52.1",
4
4
  "license": "Moderne Proprietary",
5
5
  "description": "OpenRewrite recipes for Node.js library migrations.",
6
6
  "homepage": "https://github.com/moderneinc/rewrite-node",
@@ -25,7 +25,7 @@
25
25
  "ci:test": "jest"
26
26
  },
27
27
  "dependencies": {
28
- "@openrewrite/rewrite": "^8.93.0-20261009-041902",
28
+ "@openrewrite/rewrite": "^8.93.11",
29
29
  "mutative": "^1.1.0",
30
30
  "semver": "^7.7.3"
31
31
  },
package/src/index.ts CHANGED
@@ -62,6 +62,7 @@ import {MigrateSourceMapSupportToNative} from "./migrate/source-map-support/migr
62
62
  import {FindSourceMapSupportManualMigrations} from "./migrate/source-map-support/find-source-map-support-manual-migrations";
63
63
  import {EnableSourceMapsAtLaunch} from "./migrate/source-map-support/enable-source-maps-at-launch";
64
64
  import {ReplaceSourceMapSupportInstall} from "./migrate/source-map-support/replace-source-map-support-install";
65
+ import {ReplaceSourceMapSupportApi} from "./migrate/source-map-support/replace-source-map-support-api";
65
66
  import {ReplaceSourceMapSupportRegisterInPackageJson} from "./migrate/source-map-support/replace-source-map-support-register-in-package-json";
66
67
  import {ReplaceSourceMapSupportRegisterInMochaConfig} from "./migrate/source-map-support/replace-source-map-support-register-in-mocha-config";
67
68
  import {RemoveUnusedSourceMapSupportDependency} from "./migrate/source-map-support/remove-unused-source-map-support-dependency";
@@ -143,6 +144,7 @@ export async function activate(marketplace: RecipeMarketplace): Promise<void> {
143
144
  await marketplace.install(SwapHpagentDependency, Hpagent);
144
145
  await marketplace.install(MigrateSourceMapSupportToNative, SourceMapSupport);
145
146
  await marketplace.install(ReplaceSourceMapSupportInstall, SourceMapSupport);
147
+ await marketplace.install(ReplaceSourceMapSupportApi, SourceMapSupport);
146
148
  await marketplace.install(EnableSourceMapsAtLaunch, SourceMapSupport);
147
149
  await marketplace.install(ReplaceSourceMapSupportRegisterInPackageJson, SourceMapSupport);
148
150
  await marketplace.install(ReplaceSourceMapSupportRegisterInMochaConfig, SourceMapSupport);
@@ -0,0 +1,92 @@
1
+ /*
2
+ * Copyright 2026 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ /** The conventions generated code follows where a file has no formatter to lay it out, as with eslint's `standard`. */
8
+ export interface CodeStyle {
9
+ semicolons: boolean;
10
+ quote: string;
11
+ braceSpacing: boolean;
12
+ functionParenSpace: boolean;
13
+ indent: string;
14
+ /** Whether a member on a line of its own ends with a comma even when it is the last. */
15
+ trailingCommas: boolean;
16
+ /** The line length the file keeps to, past which a list goes one member per line, as Prettier lays it out. */
17
+ printWidth: number;
18
+ }
19
+
20
+ export const DEFAULT_CODE_STYLE: CodeStyle = {
21
+ semicolons: true, quote: "'", braceSpacing: false, functionParenSpace: false, indent: ' ',
22
+ trailingCommas: false, printWidth: 80
23
+ };
24
+
25
+ /** `code`, written at four spaces a level, at the file's own indent. */
26
+ export function indented(style: CodeStyle, code: string): string {
27
+ return code.replace(/^(?: {4})+/gm, levels => style.indent.repeat(levels.length / 4));
28
+ }
29
+
30
+ /** The step a nested line most often indents by: a tab, or the commonest increase in leading spaces. */
31
+ function detectIndent(source: string): string {
32
+ const steps = new Map<string, number>();
33
+ let previous = 0;
34
+ for (const line of source.split('\n')) {
35
+ if (!line.trim()) continue;
36
+ const leading = /^[ \t]*/.exec(line)![0];
37
+ if (leading.startsWith('\t')) {
38
+ steps.set('\t', (steps.get('\t') ?? 0) + 1);
39
+ } else if (leading.length > previous && leading.length - previous <= 8) {
40
+ const step = ' '.repeat(leading.length - previous);
41
+ steps.set(step, (steps.get(step) ?? 0) + 1);
42
+ }
43
+ previous = leading.startsWith('\t') ? previous : leading.length;
44
+ }
45
+ return [...steps].sort((a, b) => b[1] - a[1])[0]?.[0] ?? DEFAULT_CODE_STYLE.indent;
46
+ }
47
+
48
+ /** The narrowest common print width almost every line keeps to, since a formatter still lets strings and comments run long. */
49
+ function detectPrintWidth(lines: string[]): number {
50
+ const code = lines.filter(line => line.trim());
51
+ for (const width of [80, 100, 120]) {
52
+ if (code.filter(line => line.length > width).length <= Math.max(1, code.length / 50)) return width;
53
+ }
54
+ return Infinity;
55
+ }
56
+
57
+ /** The conventions `source` is written in, by majority; a tie keeps the default. */
58
+ export function detectCodeStyle(source: string): CodeStyle {
59
+ const count = (pattern: RegExp) => (source.match(pattern) ?? []).length;
60
+ const fallback = (yes: number, no: number, otherwise: boolean) => yes === no ? otherwise : yes > no;
61
+ return {
62
+ semicolons: fallback(count(/;[ \t]*$/gm), count(/[\w$)\]'"`][ \t]*$/gm), DEFAULT_CODE_STYLE.semicolons),
63
+ quote: count(/(?:from|require\(|import\()\s*"/g) > count(/(?:from|require\(|import\()\s*'/g) ? '"' : "'",
64
+ // `${x}` in a template literal is not a brace of the kind this asks about.
65
+ braceSpacing: fallback(count(/(?<!\$)\{ [\w$.]/g), count(/(?<!\$)\{[\w$.]/g), DEFAULT_CODE_STYLE.braceSpacing),
66
+ functionParenSpace: fallback(count(/\bfunction\s+[\w$]+\s+\(/g), count(/\bfunction\s+[\w$]+\(/g), DEFAULT_CODE_STYLE.functionParenSpace),
67
+ indent: detectIndent(source),
68
+ // The last member of a multi-line object or array, with a comma after it or without. One ending in
69
+ // `}` or `)` is left out of the second count, which would otherwise take every block's closing brace.
70
+ trailingCommas: fallback(count(/[\w$'"\])}],[ \t]*\n[ \t]*[}\]]/g), count(/[\w$'"\]][ \t]*\n[ \t]*[}\]]/g),
71
+ DEFAULT_CODE_STYLE.trailingCommas),
72
+ printWidth: detectPrintWidth(source.split('\n')),
73
+ };
74
+ }
75
+
76
+ /**
77
+ * `code`, written in the default style, restyled to `style`. The code must be written so the edits are
78
+ * safe: outside comments, which take whole lines, `'` appears only as a string delimiter and `"` not at
79
+ * all; no line starts with `(`, `[` or a template literal; a statement-ending `;` is the last character of
80
+ * its line; and no regular expression has a `{n}` quantifier.
81
+ */
82
+ export function restyle(code: string, style: CodeStyle): string {
83
+ return indented(style, code).split('\n').map(line => {
84
+ if (/^\s*(?:\/\/|\/\*|\*)/.test(line)) return line;
85
+ let result = line;
86
+ if (!style.semicolons) result = result.replace(/;$/, '');
87
+ if (style.quote !== "'") result = result.replace(/'/g, style.quote);
88
+ if (style.braceSpacing) result = result.replace(/(?<!\$)\{([^{}]*[^{}\s])\}/g, (_, inner: string) => `{ ${inner.trim()} }`);
89
+ if (style.functionParenSpace) result = result.replace(/\bfunction ([\w$]+)\(/g, 'function $1 (');
90
+ return result;
91
+ }).join('\n');
92
+ }
@@ -96,8 +96,8 @@ function destructuredMembers(pattern: J): [string, string][] | undefined {
96
96
  }
97
97
 
98
98
  /**
99
- * `maybeRebind` won't move a destructured `require`, so one whose every member needs the same node
100
- * module gets its specifier swapped instead.
99
+ * A destructured `require` whose every member needs the same node module gets its specifier swapped
100
+ * in place, keeping the member order that `maybeRebind` would otherwise shuffle.
101
101
  */
102
102
  function retargetRequires(cu: JS.CompilationUnit): JS.CompilationUnit {
103
103
  let changed = false;
@@ -6,34 +6,24 @@
6
6
 
7
7
  import {Cursor, ExecutionContext, foundSearchResult, Recipe, TreeVisitor} from "@openrewrite/rewrite";
8
8
  import {isIdentifier, J} from "@openrewrite/rewrite/java";
9
- import {compilationUnitOf, JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
9
+ import {isReference, JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
10
10
  import {
11
- bindingsOf,
12
- classifyOption,
11
+ constructionArgument,
12
+ emptyHpagentBindings,
13
13
  HPAGENT_CLASSES,
14
14
  HPAGENT_MODULES,
15
- HPAGENT_TYPES,
15
+ hpagentBindings,
16
+ HpagentBindings,
16
17
  hpagentManualMigrationSteps,
17
- isKnownProxyRequestOptions,
18
- isModuleScoped,
19
- isObjectLiteral,
20
- ModuleBindings,
21
- objectMembers,
18
+ isMockCall,
19
+ isValueUse,
22
20
  REASONS,
21
+ resolvesToBinding,
23
22
  sourcePathOf,
24
- spreadIsPlainAgentOptions,
23
+ spiedClass,
25
24
  stringLiteral,
26
25
  } from "./hpagent-support";
27
26
 
28
- const MOCK_OWNERS = new Set(['vi', 'jest']);
29
- const MOCK_METHODS = new Set(['mock', 'doMock', 'unmock', 'unstable_mockModule']);
30
-
31
- /** Trees that hold an identifier as a declaration or import rather than a use of it. */
32
- const DECLARING_KINDS = new Set<string>([
33
- JS.Kind.Import, JS.Kind.ImportClause, JS.Kind.ImportSpecifier, JS.Kind.Alias, JS.Kind.NamedImports,
34
- JS.Kind.ExportSpecifier, JS.Kind.BindingElement, JS.Kind.ObjectBindingPattern,
35
- ]);
36
-
37
27
  function isHpagentSpecifier(e: J | undefined): boolean {
38
28
  return HPAGENT_MODULES.has(stringLiteral(e) ?? '');
39
29
  }
@@ -45,18 +35,13 @@ function isDynamicHpagentImport(e: J | undefined): boolean {
45
35
  return !!fn && isIdentifier(fn) && fn.simpleName === 'import' && isHpagentSpecifier(call.arguments.elements[0]?.element);
46
36
  }
47
37
 
48
- function isMockCall(m: J.MethodInvocation): boolean {
49
- const select = m.select?.element;
50
- return !!select && isIdentifier(select) && MOCK_OWNERS.has(select.simpleName) && MOCK_METHODS.has(m.name.simpleName);
51
- }
52
-
53
38
  export class FindHpagentManualMigrations extends Recipe {
54
39
  readonly name = "org.openrewrite.node.migrate.hpagent.find-hpagent-manual-migrations";
55
40
  readonly displayName = "Find hpagent usages that need manual migration";
56
- readonly description = "Marks every hpagent usage the automated migration leaves in place — a construction whose " +
57
- "options aren't an object literal or set a TLS option for the destination handshake, an agent class used as a " +
58
- "value, hpagent's option types, re-exports, dynamic imports and test mocks — and records each with a suggested " +
59
- "replacement in the `HpagentManualMigrationSteps` data table.";
41
+ readonly description = "Marks every hpagent usage the automated migration leaves in place — a construction " +
42
+ "that doesn't pass one options object, an agent class used as a value, re-exports, dynamic imports, a " +
43
+ "`require` in a shape it doesn't follow, and test mocks — " +
44
+ "and records each with a suggested replacement in the `HpagentManualMigrationSteps` data table.";
60
45
  readonly tags = ["hpagent", "https-proxy-agent"];
61
46
 
62
47
  private readonly steps = hpagentManualMigrationSteps();
@@ -66,7 +51,7 @@ export class FindHpagentManualMigrations extends Recipe {
66
51
  const steps = this.steps;
67
52
 
68
53
  return new class extends JavaScriptVisitor<ExecutionContext> {
69
- private bindings: ModuleBindings = new Map();
54
+ private found: HpagentBindings = emptyHpagentBindings();
70
55
  /** Rows already written for this file, so a class constructed many times reports once per reason. */
71
56
  private reported = new Set<string>();
72
57
 
@@ -85,44 +70,14 @@ export class FindHpagentManualMigrations extends Recipe {
85
70
  }
86
71
 
87
72
  protected override async visitJsCompilationUnit(cu: JS.CompilationUnit, p: ExecutionContext): Promise<J | undefined> {
88
- this.bindings = bindingsOf(cu, HPAGENT_MODULES);
73
+ this.found = hpagentBindings(cu);
89
74
  this.reported = new Set();
90
75
  return super.visitJsCompilationUnit(cu, p);
91
76
  }
92
77
 
93
- /** Whether `name` binds one of hpagent's agent classes at module scope. */
94
- private isAgentClass(name: string): boolean {
95
- const member = this.bindings.get(name);
96
- return member !== undefined && HPAGENT_CLASSES.has(member) && isModuleScoped(this.cursor, name);
97
- }
98
-
99
78
  private isWholeModule(name: string): boolean {
100
- return this.bindings.has(name) && this.bindings.get(name) === undefined && isModuleScoped(this.cursor, name);
101
- }
102
-
103
- protected override async visitImportDeclaration(jsImport: JS.Import, p: ExecutionContext): Promise<J | undefined> {
104
- const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
105
- const clause = imp.importClause;
106
- if (!isHpagentSpecifier(imp.moduleSpecifier?.element) || !clause) return imp;
107
- const named = clause.namedBindings;
108
- const specifiers = named?.kind === JS.Kind.NamedImports
109
- ? (named as JS.NamedImports).elements.elements.map(e => e.element as JS.ImportSpecifier)
110
- : [];
111
- if (clause.typeOnly || specifiers.some(spec => spec.kind === JS.Kind.ImportSpecifier && spec.importType?.element)) {
112
- return this.report(imp, 'import type', REASONS.type, p);
113
- }
114
- // hpagent's option interfaces, imported as values in a JavaScript file or erased in TypeScript.
115
- if (specifiers.some(spec => HPAGENT_TYPES.has(this.specifierMember(spec) ?? ''))) {
116
- return this.report(imp, 'options type', REASONS.type, p);
117
- }
118
- return clause.name?.element ? this.report(imp, 'default import', REASONS.defaultImport, p) : imp;
119
- }
120
-
121
- private specifierMember(spec: JS.ImportSpecifier): string | undefined {
122
- if (spec.kind !== JS.Kind.ImportSpecifier) return undefined;
123
- const specifier = spec.specifier;
124
- if (isIdentifier(specifier)) return specifier.simpleName;
125
- return specifier.kind === JS.Kind.Alias ? (specifier as JS.Alias).propertyName.element.simpleName : undefined;
79
+ return this.found.bindings.has(name) && this.found.bindings.get(name) === undefined &&
80
+ resolvesToBinding(this.cursor, name, this.found);
126
81
  }
127
82
 
128
83
  protected override async visitExportDeclaration(exportDeclaration: JS.ExportDeclaration, p: ExecutionContext): Promise<J | undefined> {
@@ -146,7 +101,7 @@ export class FindHpagentManualMigrations extends Recipe {
146
101
  if (isMockCall(m) && (isHpagentSpecifier(firstArg) || isDynamicHpagentImport(firstArg))) {
147
102
  return this.report(m, `${(m.select!.element as J.Identifier).simpleName}.${m.name.simpleName}`, REASONS.mock, p);
148
103
  }
149
- if (!m.select && m.name.simpleName === 'require' && isHpagentSpecifier(firstArg)) {
104
+ if (!m.select && m.name.simpleName === 'require' && isHpagentSpecifier(firstArg) && !this.found.requires.has(method.id)) {
150
105
  const scope = this.cursor.firstEnclosing((t: any): t is J =>
151
106
  t?.kind === JS.Kind.CompilationUnit || t?.kind === J.Kind.Block);
152
107
  return scope?.kind === J.Kind.Block ? this.report(m, 'require', REASONS.nestedRequire, p) : m;
@@ -156,99 +111,47 @@ export class FindHpagentManualMigrations extends Recipe {
156
111
 
157
112
  override async visitNewClass(newClass: J.NewClass, p: ExecutionContext): Promise<J | undefined> {
158
113
  const n = await super.visitNewClass(newClass, p) as J.NewClass;
159
- if (n?.kind !== J.Kind.NewClass || !n.class) return n;
160
- const name = this.constructedClass(n.class);
161
- if (!name) return n;
162
- const reason = this.blockingReason(n);
163
- return reason ? this.report(n, reason.usage, reason.reason, p) : n;
114
+ if (n?.kind !== J.Kind.NewClass || !n.class || !this.constructsHpagent(n.class)) return n;
115
+ // Any one options object goes to the two-argument form or the adapter; nothing else has a shape for either.
116
+ return constructionArgument(n) ? n : this.report(n, 'agent options', REASONS.arguments, p);
164
117
  }
165
118
 
166
- /** The hpagent class a `new` site constructs, through a class binding or the module's own. */
167
- private constructedClass(clazz: J): string | undefined {
168
- if (isIdentifier(clazz)) return this.isAgentClass(clazz.simpleName) ? clazz.simpleName : undefined;
169
- if (clazz.kind !== J.Kind.FieldAccess) return undefined;
170
- const access = clazz as J.FieldAccess;
171
- const member = access.name.element.simpleName;
172
- return isIdentifier(access.target) && this.isWholeModule(access.target.simpleName) && HPAGENT_CLASSES.has(member)
173
- ? member
174
- : undefined;
175
- }
176
-
177
- /** Why `ReplaceHpagent` would leave this construction alone, or undefined where it wouldn't. */
178
- private blockingReason(n: J.NewClass): {usage: string; reason: string} | undefined {
179
- const args = n.arguments.elements.map(a => a.element).filter(a => a.kind !== J.Kind.Empty);
180
- if (args.length !== 1 || !isObjectLiteral(args[0])) {
181
- return {usage: 'agent options', reason: REASONS.notLiteral};
182
- }
183
- let hasProxy = false;
184
- let hasSpread = false;
185
- let blocking: {usage: string; reason: string} | undefined;
186
- for (const statement of objectMembers(args[0])) {
187
- const option = classifyOption(statement);
188
- if (option.kind === 'proxy') hasProxy = true;
189
- // A spread that resolves to plain agent options is migrated, so it is not reported.
190
- if (option.kind === 'spread' && !spreadIsPlainAgentOptions(compilationUnitOf(this)!, option.value)) {
191
- hasSpread = true;
192
- }
193
- if (option.kind === 'proxyRequestOptions' &&
194
- (!isObjectLiteral(option.value) || !isKnownProxyRequestOptions(option.value))) {
195
- blocking ??= {usage: 'proxyRequestOptions', reason: REASONS.notLiteral};
196
- }
197
- if (option.kind === 'unmappable') blocking ??= {usage: option.option, reason: option.reason};
119
+ private constructsHpagent(clazz: J): boolean {
120
+ if (isIdentifier(clazz)) {
121
+ const member = this.found.bindings.get(clazz.simpleName);
122
+ return member !== undefined && HPAGENT_CLASSES.has(member) && resolvesToBinding(this.cursor, clazz.simpleName, this.found);
198
123
  }
199
- // A named option the migration can't place is the more actionable report than the spread around it.
200
- if (blocking) return blocking;
201
- if (hasSpread) return {usage: '...', reason: hasProxy ? REASONS.spread : REASONS.spreadNoProxy};
202
- return hasProxy ? undefined : {usage: 'agent options', reason: REASONS.noProxy};
124
+ if (clazz.kind !== J.Kind.FieldAccess) return false;
125
+ const access = clazz as J.FieldAccess;
126
+ return isIdentifier(access.target) && this.isWholeModule(access.target.simpleName) &&
127
+ HPAGENT_CLASSES.has(access.name.element.simpleName);
203
128
  }
204
129
 
205
- /**
206
- * `hpagent.HttpProxyAgent` reached through a binding of the whole module, where it is not being
207
- * constructed: a type annotation, an `instanceof`, or the class passed along as a value. Each keeps
208
- * the hpagent import alive, so each has to be moved by hand.
209
- */
130
+ /** `hpagent.HttpProxyAgent` handed on as a value through a binding of the whole module. */
210
131
  override async visitFieldAccess(fieldAccess: J.FieldAccess, p: ExecutionContext): Promise<J | undefined> {
211
132
  const fa = await super.visitFieldAccess(fieldAccess, p) as J.FieldAccess;
212
- const target = fa.target;
213
- if (!isIdentifier(target) || !this.isWholeModule(target.simpleName)) return fa;
133
+ if (!isIdentifier(fa.target) || !this.isWholeModule(fa.target.simpleName) || this.found.typeOnly.has(fa.target.simpleName)) return fa;
214
134
  const name = fa.name.element.simpleName;
215
- // A construction through it is the rewrite's business, and reported there if it can't be made.
216
- if (this.cursor.parentTree()?.value?.kind === J.Kind.NewClass) return fa;
217
- if (HPAGENT_TYPES.has(name) || this.inTypePosition()) return this.report(fa, name, REASONS.type, p);
218
- return HPAGENT_CLASSES.has(name) ? this.report(fa, name, REASONS.value, p) : fa;
135
+ return HPAGENT_CLASSES.has(name) && isValueUse(this.cursor) ? this.report(fa, name, REASONS.value, p) : fa;
219
136
  }
220
137
 
221
138
  override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
222
139
  const id = await super.visitIdentifier(identifier, p) as J.Identifier;
223
- if (!this.bindings.has(id.simpleName) || !isModuleScoped(this.cursor, id.simpleName)) return id;
224
- if (!this.isValueUse(identifier)) return id;
225
- // An option type names a type wherever it stands, including in a cast the cursor can't see into.
226
- const type = HPAGENT_TYPES.has(id.simpleName) || this.inTypePosition();
227
- return this.report(id, id.simpleName, type ? REASONS.type : REASONS.value, p);
228
- }
229
-
230
- /** Whether `id` stands for the value it names, not a construction, member access, declaration or import. */
231
- private isValueUse(id: J.Identifier): boolean {
140
+ const bindings = this.found.bindings;
141
+ if (!bindings.has(id.simpleName) || this.found.assignmentTargets.has(identifier.id) || this.found.typeOnly.has(id.simpleName) ||
142
+ !isReference(this.cursor, identifier) || !resolvesToBinding(this.cursor, id.simpleName, this.found)) {
143
+ return id;
144
+ }
145
+ const member = bindings.get(id.simpleName);
146
+ // The adapter's types stand in for hpagent's option types.
147
+ if (member !== undefined && !HPAGENT_CLASSES.has(member)) return id;
148
+ // `jest.spyOn(hpagent, 'HttpsProxyAgent')` reads the class off by a name the new module exports too.
232
149
  const parent = this.cursor.parentTree()?.value as J | undefined;
233
- if (!parent || DECLARING_KINDS.has(parent.kind)) return false;
234
- switch (parent.kind) {
235
- case J.Kind.NewClass:
236
- return (parent as J.NewClass).class !== id;
237
- case J.Kind.FieldAccess:
238
- return (parent as J.FieldAccess).target !== id;
239
- case J.Kind.MethodInvocation: {
240
- const m = parent as J.MethodInvocation;
241
- return m.select?.element !== id && m.name !== id;
242
- }
243
- case J.Kind.NamedVariable:
244
- return (parent as J.VariableDeclarations.NamedVariable).name !== id;
245
- default:
246
- return true;
150
+ if (member === undefined && parent?.kind === J.Kind.MethodInvocation &&
151
+ spiedClass(parent as J.MethodInvocation) !== undefined) {
152
+ return id;
247
153
  }
248
- }
249
-
250
- private inTypePosition(): boolean {
251
- return !!this.cursor.firstEnclosing((t: any): t is J => t?.kind === JS.Kind.TypeInfo);
154
+ return isValueUse(this.cursor) ? this.report(id, id.simpleName, REASONS.value, p) : id;
252
155
  }
253
156
  };
254
157
  }