shapi-cli 0.1.0 → 0.2.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/README.md +2 -1
- package/advisories/anthropic-api-version-2026-10-21-sampling-params-400.json +35 -0
- package/advisories/google-ads-api-v22-sunset.json +27 -0
- package/advisories/google-custom-search-json-api-shutdown.json +27 -0
- package/advisories/google-gemini-2-0-flash-shutdown.json +27 -0
- package/advisories/google-gemini-2-5-flash-image-shutdown.json +27 -0
- package/advisories/google-gemini-2-5-preview-tts-shutdown.json +27 -0
- package/advisories/hubspot-contact-lists-v1-sunset.json +28 -0
- package/advisories/hubspot-contacts-v1-list-memberships-removed.json +30 -0
- package/advisories/hubspot-marketing-emails-folderid-removed.json +39 -0
- package/advisories/hubspot-marketing-emails-v1-sunset.json +24 -0
- package/advisories/hubspot-pipelines-v1-sunset.json +22 -2
- package/advisories/meta-graph-commerce-order-management-removed.json +26 -0
- package/advisories/meta-graph-date-format-param-removed.json +30 -0
- package/advisories/meta-graph-delivery-estimate-fields-removed.json +30 -0
- package/advisories/meta-graph-instagram-oembed-fields-removed.json +33 -0
- package/advisories/meta-graph-marketing-api-v24-sunset.json +34 -0
- package/advisories/meta-graph-marketing-api-v25-sunset.json +35 -0
- package/advisories/meta-graph-page-insights-impressions-fans-removed.json +51 -0
- package/advisories/meta-graph-page-insights-reach-video-views-removed.json +57 -0
- package/advisories/meta-graph-root-ids-lookup-removed.json +30 -0
- package/advisories/meta-graph-v20-sunset.json +10 -1
- package/advisories/meta-graph-v21-sunset.json +10 -1
- package/advisories/openai-assistants-api-shutdown.json +27 -0
- package/advisories/openai-audio-translations-endpoint-shutdown.json +27 -0
- package/advisories/openai-evals-api-shutdown.json +27 -0
- package/advisories/quickbooks-companyinfo-neoenabled-removed.json +29 -0
- package/advisories/quickbooks-minorversion-below-75-ignored.json +13 -1
- package/advisories/quickbooks-refresh-token-five-year-hard-expiry.json +29 -0
- package/advisories/salesforce-soap-login-broad-ip-range-token-required.json +30 -0
- package/advisories/salesforce-soap-login-disabled-when-unused.json +30 -0
- package/advisories/salesforce-soap-login-use-any-api-auth-required.json +31 -0
- package/advisories/shopify-admin-api-2026-01-unsupported.json +33 -0
- package/advisories/shopify-admin-api-2026-04-unsupported.json +33 -0
- package/advisories/shopify-global-catalog-rest-api-shutdown.json +24 -0
- package/advisories/shopify-non-expiring-offline-tokens-rejected.json +27 -0
- package/advisories/shopify-script-tags-stop-running.json +27 -0
- package/advisories/slack-files-upload-retired.json +28 -0
- package/advisories/stripe-checkout-session-ui-mode-values-renamed.json +53 -0
- package/advisories/twilio-conference-list-default-in-progress.json +27 -0
- package/advisories/twilio-notify-end-of-life.json +28 -0
- package/advisories/twilio-rcs-undelivered-error-code-30046.json +27 -0
- package/advisories/twilio-voice-insights-conference-participant-removed.json +28 -0
- package/advisories/x-activity-api-pascalcase-event-types-removed.json +31 -0
- package/advisories/x-self-serve-follow-writes-removed.json +27 -0
- package/advisories/x-self-serve-like-writes-removed.json +27 -0
- package/advisories/xero-allocations-contact-mismatch-rejected.json +27 -0
- package/advisories/xero-au-payroll-isqualifyingearnings-required.json +28 -0
- package/advisories/xero-finance-accounting-activities-decommissioned.json +28 -0
- package/advisories/xero-invoices-email-daily-limit-429.json +32 -0
- package/advisories/xero-nz-payroll-employee-tax-kiwisaverstatus-required.json +27 -0
- package/advisories/xero-organisation-class-ultra-enum.json +31 -0
- package/dist/index.js +1333 -153
- package/dist/tree-sitter-python.wasm +0 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Command as Command2, CommanderError as CommanderError2, Option } from "
|
|
|
3
3
|
|
|
4
4
|
// packages/cli/src/fix.ts
|
|
5
5
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
6
|
-
import { existsSync as
|
|
6
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync3, readdirSync as readdirSync3, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
7
7
|
import os3 from "os";
|
|
8
8
|
import path12 from "path";
|
|
9
9
|
|
|
@@ -51,10 +51,34 @@ var providers_default = [
|
|
|
51
51
|
"subscriptions.update": "POST /v1/subscriptions/{id}",
|
|
52
52
|
"invoices.create": "POST /v1/invoices",
|
|
53
53
|
"checkout.sessions.create": "POST /v1/checkout/sessions",
|
|
54
|
+
"checkout.sessions.retrieve": "GET /v1/checkout/sessions/{session}",
|
|
54
55
|
"refunds.create": "POST /v1/refunds"
|
|
55
56
|
},
|
|
56
57
|
docsUrl: "https://docs.stripe.com/api",
|
|
57
|
-
changelogUrl: "https://docs.stripe.com/changelog"
|
|
58
|
+
changelogUrl: "https://docs.stripe.com/changelog",
|
|
59
|
+
python: {
|
|
60
|
+
packages: [
|
|
61
|
+
{ pypi: "stripe", module: "stripe" }
|
|
62
|
+
],
|
|
63
|
+
methods: {
|
|
64
|
+
"checkout.Session.create": "POST /v1/checkout/sessions",
|
|
65
|
+
"checkout.Session.retrieve": "GET /v1/checkout/sessions/{session}",
|
|
66
|
+
"PaymentIntent.create": "POST /v1/payment_intents",
|
|
67
|
+
"PaymentIntent.retrieve": "GET /v1/payment_intents/{intent}",
|
|
68
|
+
"PaymentIntent.confirm": "POST /v1/payment_intents/{intent}/confirm",
|
|
69
|
+
"SetupIntent.create": "POST /v1/setup_intents",
|
|
70
|
+
"Customer.create": "POST /v1/customers",
|
|
71
|
+
"Customer.list": "GET /v1/customers",
|
|
72
|
+
"Subscription.create": "POST /v1/subscriptions",
|
|
73
|
+
"Balance.retrieve": "GET /v1/balance",
|
|
74
|
+
"checkout.sessions.create": "POST /v1/checkout/sessions",
|
|
75
|
+
"checkout.sessions.retrieve": "GET /v1/checkout/sessions/{session}",
|
|
76
|
+
"payment_intents.create": "POST /v1/payment_intents",
|
|
77
|
+
"payment_intents.retrieve": "GET /v1/payment_intents/{intent}",
|
|
78
|
+
"customers.create": "POST /v1/customers",
|
|
79
|
+
"customers.list": "GET /v1/customers"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
58
82
|
},
|
|
59
83
|
{
|
|
60
84
|
id: "twilio",
|
|
@@ -66,12 +90,23 @@ var providers_default = [
|
|
|
66
90
|
"calls.create": "POST /2010-04-01/Accounts/{AccountSid}/Calls.json"
|
|
67
91
|
},
|
|
68
92
|
docsUrl: "https://www.twilio.com/docs/usage/api",
|
|
69
|
-
changelogUrl: "https://www.twilio.com/en-us/changelog"
|
|
93
|
+
changelogUrl: "https://www.twilio.com/en-us/changelog",
|
|
94
|
+
python: {
|
|
95
|
+
packages: [
|
|
96
|
+
{ pypi: "twilio", module: "twilio" }
|
|
97
|
+
],
|
|
98
|
+
methods: {
|
|
99
|
+
"messages.create": "POST /2010-04-01/Accounts/{AccountSid}/Messages.json",
|
|
100
|
+
"messages.list": "GET /2010-04-01/Accounts/{AccountSid}/Messages.json",
|
|
101
|
+
"calls.create": "POST /2010-04-01/Accounts/{AccountSid}/Calls.json",
|
|
102
|
+
"conferences.list": "GET /2010-04-01/Accounts/{AccountSid}/Conferences.json"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
70
105
|
},
|
|
71
106
|
{
|
|
72
107
|
id: "shopify",
|
|
73
108
|
name: "Shopify",
|
|
74
|
-
hosts: ["*.myshopify.com"],
|
|
109
|
+
hosts: ["*.myshopify.com", "discover.shopifyapps.com"],
|
|
75
110
|
sdkPackages: ["@shopify/shopify-api", "@shopify/admin-api-client", "shopify-api-node"],
|
|
76
111
|
docsUrl: "https://shopify.dev/docs/api/admin",
|
|
77
112
|
changelogUrl: "https://shopify.dev/changelog"
|
|
@@ -98,7 +133,18 @@ var providers_default = [
|
|
|
98
133
|
hosts: ["api.hubapi.com", "api.hubspot.com"],
|
|
99
134
|
sdkPackages: ["@hubspot/api-client"],
|
|
100
135
|
docsUrl: "https://developers.hubspot.com/docs/api/overview",
|
|
101
|
-
changelogUrl: "https://developers.hubspot.com/changelog"
|
|
136
|
+
changelogUrl: "https://developers.hubspot.com/changelog",
|
|
137
|
+
python: {
|
|
138
|
+
packages: [
|
|
139
|
+
{ pypi: "hubspot-api-client", module: "hubspot" }
|
|
140
|
+
],
|
|
141
|
+
methods: {
|
|
142
|
+
"crm.contacts.basic_api.get_page": "GET /crm/v3/objects/contacts",
|
|
143
|
+
"crm.contacts.basic_api.create": "POST /crm/v3/objects/contacts",
|
|
144
|
+
"crm.deals.basic_api.get_page": "GET /crm/v3/objects/deals",
|
|
145
|
+
"crm.pipelines.pipelines_api.get_all": "GET /crm/v3/pipelines/{objectType}"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
102
148
|
},
|
|
103
149
|
{
|
|
104
150
|
id: "slack",
|
|
@@ -114,7 +160,12 @@ var providers_default = [
|
|
|
114
160
|
"files.uploadV2": "POST /api/files.uploadV2"
|
|
115
161
|
},
|
|
116
162
|
docsUrl: "https://api.slack.com/methods",
|
|
117
|
-
changelogUrl: "https://api.slack.com/changelog"
|
|
163
|
+
changelogUrl: "https://api.slack.com/changelog",
|
|
164
|
+
python: {
|
|
165
|
+
packages: [
|
|
166
|
+
{ pypi: "slack-sdk", module: "slack_sdk" }
|
|
167
|
+
]
|
|
168
|
+
}
|
|
118
169
|
},
|
|
119
170
|
{
|
|
120
171
|
id: "openai",
|
|
@@ -132,7 +183,27 @@ var providers_default = [
|
|
|
132
183
|
"files.create": "POST /v1/files"
|
|
133
184
|
},
|
|
134
185
|
docsUrl: "https://platform.openai.com/docs/api-reference",
|
|
135
|
-
changelogUrl: "https://platform.openai.com/docs/changelog"
|
|
186
|
+
changelogUrl: "https://platform.openai.com/docs/changelog",
|
|
187
|
+
python: {
|
|
188
|
+
packages: [
|
|
189
|
+
{ pypi: "openai", module: "openai" }
|
|
190
|
+
],
|
|
191
|
+
methods: {
|
|
192
|
+
"chat.completions.create": "POST /v1/chat/completions",
|
|
193
|
+
"responses.create": "POST /v1/responses",
|
|
194
|
+
"models.list": "GET /v1/models",
|
|
195
|
+
"embeddings.create": "POST /v1/embeddings",
|
|
196
|
+
"audio.translations.create": "POST /v1/audio/translations",
|
|
197
|
+
"audio.transcriptions.create": "POST /v1/audio/transcriptions",
|
|
198
|
+
"images.generate": "POST /v1/images/generations",
|
|
199
|
+
"beta.assistants.create": "POST /v1/assistants",
|
|
200
|
+
"beta.assistants.list": "GET /v1/assistants",
|
|
201
|
+
"beta.threads.create": "POST /v1/threads",
|
|
202
|
+
"beta.threads.runs.create": "POST /v1/threads/{thread_id}/runs",
|
|
203
|
+
"evals.create": "POST /v1/evals",
|
|
204
|
+
"evals.list": "GET /v1/evals"
|
|
205
|
+
}
|
|
206
|
+
}
|
|
136
207
|
},
|
|
137
208
|
{
|
|
138
209
|
id: "anthropic",
|
|
@@ -146,7 +217,19 @@ var providers_default = [
|
|
|
146
217
|
"models.list": "GET /v1/models"
|
|
147
218
|
},
|
|
148
219
|
docsUrl: "https://docs.claude.com/en/api",
|
|
149
|
-
changelogUrl: "https://docs.claude.com/en/release-notes/api"
|
|
220
|
+
changelogUrl: "https://docs.claude.com/en/release-notes/api",
|
|
221
|
+
python: {
|
|
222
|
+
packages: [
|
|
223
|
+
{ pypi: "anthropic", module: "anthropic" }
|
|
224
|
+
],
|
|
225
|
+
methods: {
|
|
226
|
+
"messages.create": "POST /v1/messages",
|
|
227
|
+
"messages.stream": "POST /v1/messages",
|
|
228
|
+
"messages.count_tokens": "POST /v1/messages/count_tokens",
|
|
229
|
+
"messages.batches.create": "POST /v1/messages/batches",
|
|
230
|
+
"models.list": "GET /v1/models"
|
|
231
|
+
}
|
|
232
|
+
}
|
|
150
233
|
},
|
|
151
234
|
{
|
|
152
235
|
id: "salesforce",
|
|
@@ -357,7 +440,12 @@ var ProviderSchema = z.object({
|
|
|
357
440
|
/** SDK method chain (after the client, without a leading "rest.") to the HTTP endpoint it calls. */
|
|
358
441
|
sdkMethods: z.record(z.string(), EndpointRefSchema).optional(),
|
|
359
442
|
docsUrl: z.url(),
|
|
360
|
-
changelogUrl: z.url().optional()
|
|
443
|
+
changelogUrl: z.url().optional(),
|
|
444
|
+
/** The provider's Python SDK: PyPI package and import module, and SDK method chain → HTTP endpoint. */
|
|
445
|
+
python: z.object({
|
|
446
|
+
packages: z.array(z.object({ pypi: z.string().min(1), module: z.string().min(1) })),
|
|
447
|
+
methods: z.record(z.string(), EndpointRefSchema).optional()
|
|
448
|
+
}).optional()
|
|
361
449
|
});
|
|
362
450
|
var ProviderRegistrySchema = z.array(ProviderSchema).refine((ps) => new Set(ps.map((p) => p.id)).size === ps.length, "provider ids must be unique");
|
|
363
451
|
var cached;
|
|
@@ -365,6 +453,9 @@ function loadProviders() {
|
|
|
365
453
|
cached ??= ProviderRegistrySchema.parse(providers_default);
|
|
366
454
|
return cached;
|
|
367
455
|
}
|
|
456
|
+
function getProvider(id) {
|
|
457
|
+
return loadProviders().find((p) => p.id === id);
|
|
458
|
+
}
|
|
368
459
|
function providerForHost(host) {
|
|
369
460
|
const h = host.toLowerCase().replace(/:\d+$/, "");
|
|
370
461
|
let best;
|
|
@@ -381,6 +472,14 @@ function providerForHost(host) {
|
|
|
381
472
|
function providerForPackage(name) {
|
|
382
473
|
return loadProviders().find((p) => p.sdkPackages.includes(name));
|
|
383
474
|
}
|
|
475
|
+
function providerForPythonModule(module) {
|
|
476
|
+
const top = module.split(".")[0];
|
|
477
|
+
return loadProviders().find((p) => p.python?.packages.some((pkg) => pkg.module === top));
|
|
478
|
+
}
|
|
479
|
+
function providerForPythonPackage(name) {
|
|
480
|
+
const norm = (s) => s.toLowerCase().replace(/[-_.]+/g, "-");
|
|
481
|
+
return loadProviders().find((p) => p.python?.packages.some((pkg) => norm(pkg.pypi) === norm(name)));
|
|
482
|
+
}
|
|
384
483
|
function sdkEndpoint(provider, chain) {
|
|
385
484
|
const methods = provider.sdkMethods ?? {};
|
|
386
485
|
const key = chain.replace(/^rest\./, "");
|
|
@@ -516,11 +615,26 @@ var TriggerSchema = z3.object({
|
|
|
516
615
|
enums: z3.array(z3.object({ field: z3.string() })).optional(),
|
|
517
616
|
/** Affected when the request sets this header (optionally to one of these values). */
|
|
518
617
|
headers: z3.array(z3.object({ name: z3.string(), values: z3.array(z3.string()).optional() })).optional(),
|
|
519
|
-
/**
|
|
520
|
-
|
|
618
|
+
/**
|
|
619
|
+
* Affected when the request sets this query parameter: optionally to a number below `below`, or to
|
|
620
|
+
* one of `values` (a comma-separated value such as `metric=a,b` matches when any item does).
|
|
621
|
+
*/
|
|
622
|
+
query: z3.array(z3.object({ name: z3.string(), below: z3.number().optional(), values: z3.array(z3.string()).optional() })).optional(),
|
|
521
623
|
/** Every call to the endpoint is affected (sunsets, auth and scope changes). */
|
|
522
624
|
any: z3.boolean().optional()
|
|
523
625
|
}).refine((t) => Object.values(t).some((v) => v !== void 0 && v !== false), "trigger needs at least one condition");
|
|
626
|
+
var FixSchema = z3.object({
|
|
627
|
+
/** Replace this part of the request path, for example "/v21.0/" with "/v26.0/". */
|
|
628
|
+
path: z3.object({ from: z3.string().min(1), to: z3.string().min(1) }).optional(),
|
|
629
|
+
/** Send this query parameter with this value, for example minorversion=75. */
|
|
630
|
+
query: z3.object({ name: z3.string().min(1), value: z3.string() }).optional(),
|
|
631
|
+
/** Send this request header with this value, for example Stripe-Version. */
|
|
632
|
+
header: z3.object({ name: z3.string().min(1), value: z3.string() }).optional(),
|
|
633
|
+
/** Response fields that were renamed: the code reads the new name, falling back to the old one. */
|
|
634
|
+
fields: z3.array(z3.object({ from: z3.string().min(1), to: z3.string().min(1) })).optional(),
|
|
635
|
+
/** Enum values that were renamed: comparisons with an old value also accept the new one. */
|
|
636
|
+
enums: z3.array(z3.object({ field: z3.string().min(1), values: z3.array(z3.object({ from: z3.string(), to: z3.string() })).min(1) })).optional()
|
|
637
|
+
}).refine((f) => Object.values(f).some((v) => v !== void 0), "fix needs at least one change");
|
|
524
638
|
var AdvisorySchema = z3.object({
|
|
525
639
|
id: z3.string().regex(/^[a-z0-9-]+$/),
|
|
526
640
|
provider: z3.string(),
|
|
@@ -538,6 +652,8 @@ var AdvisorySchema = z3.object({
|
|
|
538
652
|
severity: z3.enum(SEVERITIES),
|
|
539
653
|
migrationNote: z3.string().min(1),
|
|
540
654
|
recipeId: z3.string().optional(),
|
|
655
|
+
/** What `recipeId: "apply-fix"` changes. */
|
|
656
|
+
fix: FixSchema.optional(),
|
|
541
657
|
sources: z3.array(z3.url()).min(1),
|
|
542
658
|
/** "verified": checked against a primary or two independent sources. "unverified": secondary reports only. */
|
|
543
659
|
status: z3.enum(["verified", "unverified"]),
|
|
@@ -646,8 +762,12 @@ function triggerReasons(site, trigger) {
|
|
|
646
762
|
for (const q of trigger.query ?? []) {
|
|
647
763
|
const p = s.query.find((x) => x.name === q.name);
|
|
648
764
|
if (!p) continue;
|
|
649
|
-
if (q.below
|
|
650
|
-
|
|
765
|
+
if (q.below !== void 0) {
|
|
766
|
+
if (p.value !== null && Number(p.value) < q.below) reasons.push(`sends ${p.name}=${p.value} (below ${q.below})`);
|
|
767
|
+
} else if (q.values) {
|
|
768
|
+
const hit = p.value === null ? [] : p.value.split(",").map((v) => v.trim()).filter((v) => q.values.includes(v));
|
|
769
|
+
if (hit.length) reasons.push(`sends ${p.name}=${hit.join(",")}`);
|
|
770
|
+
} else reasons.push(`sends ${p.name}=${p.value ?? "(dynamic)"}`);
|
|
651
771
|
}
|
|
652
772
|
if (trigger.any) reasons.push(`calls ${describe(site)}`);
|
|
653
773
|
return reasons;
|
|
@@ -843,7 +963,8 @@ function fetchStyle(ctx, call, lib, urlArg, optsArg, verb, baseURL) {
|
|
|
843
963
|
if (opts) method = methodFrom(ctx, propertyValue(opts, "method")) ?? (propertyValue(opts, "method") ? null : "GET");
|
|
844
964
|
else method = null;
|
|
845
965
|
}
|
|
846
|
-
|
|
966
|
+
const params = opts && (lib === "got" || lib === "ky") ? resolveObject(ctx, propertyValue(opts, "searchParams")) : void 0;
|
|
967
|
+
return httpSite(ctx, call, lib, joinUrl(baseURL, evalString(ctx, urlArg)), method, opts ? optsArg : void 0, params);
|
|
847
968
|
}
|
|
848
969
|
function axiosStyle(ctx, call, verb, baseURL) {
|
|
849
970
|
const args = call.getArguments();
|
|
@@ -1136,6 +1257,7 @@ var NOT_FIELDS = /* @__PURE__ */ new Set([
|
|
|
1136
1257
|
"valueOf",
|
|
1137
1258
|
"toJSON"
|
|
1138
1259
|
]);
|
|
1260
|
+
var ITEM_CALLBACKS = { map: 0, flatMap: 0, filter: 0, forEach: 0, find: 0, findIndex: 0, some: 0, every: 0, reduce: 1 };
|
|
1139
1261
|
var MAX_DEPTH = 4;
|
|
1140
1262
|
var Collector = class {
|
|
1141
1263
|
fields = /* @__PURE__ */ new Set();
|
|
@@ -1250,7 +1372,7 @@ function use(ref, loc, env, c, depth) {
|
|
|
1250
1372
|
function interpret(env, loc, access, top, c, depth, compare) {
|
|
1251
1373
|
const full = [...loc.path, ...access];
|
|
1252
1374
|
if (loc.space === "body" || env === "body") {
|
|
1253
|
-
body(full, top, c, compare);
|
|
1375
|
+
body(full, top, c, compare, env, depth);
|
|
1254
1376
|
return;
|
|
1255
1377
|
}
|
|
1256
1378
|
const [first, ...rest] = full;
|
|
@@ -1276,20 +1398,37 @@ function interpret(env, loc, access, top, c, depth, compare) {
|
|
|
1276
1398
|
} else if (h && h !== "get") c.responseHeaders.add(h.toLowerCase());
|
|
1277
1399
|
return;
|
|
1278
1400
|
}
|
|
1279
|
-
if (env === "axios" && first === "data") return body(rest, top, c, compare);
|
|
1280
|
-
if (env === "got" && first === "body") return body(rest, top, c, compare);
|
|
1401
|
+
if (env === "axios" && first === "data") return body(rest, top, c, compare, env, depth);
|
|
1402
|
+
if (env === "got" && first === "body") return body(rest, top, c, compare, env, depth);
|
|
1281
1403
|
const bodyCall = (env === "fetch" || env === "got") && rest.length === 0 && (first === "json" || first === "text") || env === "undici" && first === "body" && rest.length === 1 && (rest[0] === "json" || rest[0] === "text");
|
|
1282
1404
|
if (bodyCall && isCall()) {
|
|
1283
1405
|
bindFrom(top.getParent(), { space: "body", path: [] }, env, c, depth + 1);
|
|
1284
1406
|
}
|
|
1285
1407
|
}
|
|
1286
|
-
function body(path13, top, c, compare) {
|
|
1408
|
+
function body(path13, top, c, compare, env, depth) {
|
|
1287
1409
|
const cut = path13.findIndex((p) => NOT_FIELDS.has(p));
|
|
1288
1410
|
const fieldPath = cut >= 0 ? path13.slice(0, cut) : path13;
|
|
1289
1411
|
if (fieldPath.length === 0) return;
|
|
1290
1412
|
const field = fieldPath.join(".").replace(/\.\[\]/g, "[]");
|
|
1291
1413
|
c.fields.add(field);
|
|
1292
1414
|
if (compare && cut < 0) for (const s of comparedLiterals(top).strs) c.addEnum(field, s);
|
|
1415
|
+
if (depth >= MAX_DEPTH) return;
|
|
1416
|
+
const items = { space: "body", path: [...fieldPath, "[]"] };
|
|
1417
|
+
const method = cut >= 0 && cut === path13.length - 1 ? path13[cut] : void 0;
|
|
1418
|
+
const call = top.getParent();
|
|
1419
|
+
if (method && method in ITEM_CALLBACKS && Node2.isCallExpression(call) && call.getExpression() === top) {
|
|
1420
|
+
const cb = call.getArguments()[0];
|
|
1421
|
+
const param = cb && (Node2.isArrowFunction(cb) || Node2.isFunctionExpression(cb)) ? cb.getParameters()[ITEM_CALLBACKS[method]] : void 0;
|
|
1422
|
+
if (param) bindName(param.getNameNode(), items, param, env, c, depth + 1);
|
|
1423
|
+
if (method === "find") bindFrom(call, items, env, c, depth + 1);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
const loop = top.getParent();
|
|
1427
|
+
if (cut < 0 && Node2.isForOfStatement(loop) && loop.getExpression() === top) {
|
|
1428
|
+
const list = loop.getInitializer();
|
|
1429
|
+
const decl = Node2.isVariableDeclarationList(list) ? list.getDeclarations()[0] : void 0;
|
|
1430
|
+
if (decl) bindName(decl.getNameNode(), items, decl, env, c, depth + 1);
|
|
1431
|
+
}
|
|
1293
1432
|
}
|
|
1294
1433
|
function comparedLiterals(node) {
|
|
1295
1434
|
const nums = [];
|
|
@@ -1323,10 +1462,14 @@ function comparedLiterals(node) {
|
|
|
1323
1462
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
1324
1463
|
import path3 from "path";
|
|
1325
1464
|
var SOURCE_FILE = /\.(c|m)?[jt]sx?$/;
|
|
1326
|
-
var
|
|
1465
|
+
var PYTHON_FILE = /\.py$/;
|
|
1466
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "vendor", "bower_components", "venv", "__pycache__", "site-packages"]);
|
|
1467
|
+
var PYTHON_MANIFEST = /^(requirements[\w.-]*\.txt|pyproject\.toml)$/;
|
|
1327
1468
|
function listRepoFiles(root) {
|
|
1328
1469
|
const sources = [];
|
|
1329
1470
|
const manifests = [];
|
|
1471
|
+
const python = [];
|
|
1472
|
+
const pythonManifests = [];
|
|
1330
1473
|
const walk2 = (dir) => {
|
|
1331
1474
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
1332
1475
|
if (entry.name.startsWith(".")) continue;
|
|
@@ -1336,11 +1479,13 @@ function listRepoFiles(root) {
|
|
|
1336
1479
|
} else if (entry.isFile()) {
|
|
1337
1480
|
if (entry.name === "package.json") manifests.push(full);
|
|
1338
1481
|
else if (SOURCE_FILE.test(entry.name) && !entry.name.endsWith(".d.ts")) sources.push(full);
|
|
1482
|
+
else if (PYTHON_FILE.test(entry.name)) python.push(full);
|
|
1483
|
+
else if (PYTHON_MANIFEST.test(entry.name)) pythonManifests.push(full);
|
|
1339
1484
|
}
|
|
1340
1485
|
}
|
|
1341
1486
|
};
|
|
1342
1487
|
walk2(root);
|
|
1343
|
-
return { sources: sources.sort(), manifests: manifests.sort() };
|
|
1488
|
+
return { sources: sources.sort(), manifests: manifests.sort(), python: python.sort(), pythonManifests: pythonManifests.sort() };
|
|
1344
1489
|
}
|
|
1345
1490
|
function declaredProviders(root, manifests) {
|
|
1346
1491
|
const found = [];
|
|
@@ -1366,6 +1511,530 @@ function declaredProviders(root, manifests) {
|
|
|
1366
1511
|
function toPosix(p) {
|
|
1367
1512
|
return p.split(path3.sep).join("/");
|
|
1368
1513
|
}
|
|
1514
|
+
function declaredPythonProviders(root, manifests) {
|
|
1515
|
+
const found = [];
|
|
1516
|
+
for (const manifest of manifests) {
|
|
1517
|
+
let text;
|
|
1518
|
+
try {
|
|
1519
|
+
text = readFileSync2(manifest, "utf8");
|
|
1520
|
+
} catch {
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
const names = /* @__PURE__ */ new Set();
|
|
1524
|
+
if (manifest.endsWith(".txt")) {
|
|
1525
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1526
|
+
const m = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(line.replace(/#.*/, ""));
|
|
1527
|
+
if (m && !line.trim().startsWith("-")) names.add(m[1]);
|
|
1528
|
+
}
|
|
1529
|
+
} else {
|
|
1530
|
+
let inTable = false;
|
|
1531
|
+
let inList = false;
|
|
1532
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1533
|
+
const t = line.replace(/#.*/, "").trim();
|
|
1534
|
+
if (/^\[/.test(t)) {
|
|
1535
|
+
inTable = /dependencies\]$/.test(t);
|
|
1536
|
+
inList = false;
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
if (/^(dependencies|optional-dependencies|dev-dependencies)\s*=\s*\[/.test(t) || /^[\w-]+\s*=\s*\[/.test(t)) inList = true;
|
|
1540
|
+
if (inList) for (const m of t.matchAll(/["']([A-Za-z0-9][A-Za-z0-9._-]*)/g)) names.add(m[1]);
|
|
1541
|
+
if (inList && t.includes("]")) inList = false;
|
|
1542
|
+
const kv = inTable ? /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*=/.exec(t) : null;
|
|
1543
|
+
if (kv) names.add(kv[1]);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
for (const name of [...names].sort()) {
|
|
1547
|
+
const provider = providerForPythonPackage(name);
|
|
1548
|
+
if (provider) found.push({ provider: provider.id, package: name, manifest: toPosix(path3.relative(root, manifest)) });
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return found;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// packages/extractor/src/python.ts
|
|
1555
|
+
import { existsSync } from "fs";
|
|
1556
|
+
import { createRequire } from "module";
|
|
1557
|
+
import { fileURLToPath } from "url";
|
|
1558
|
+
import { Language, Parser } from "web-tree-sitter";
|
|
1559
|
+
var parser;
|
|
1560
|
+
async function loadPythonParser() {
|
|
1561
|
+
if (parser) return;
|
|
1562
|
+
await Parser.init();
|
|
1563
|
+
const bundled = fileURLToPath(new URL("./tree-sitter-python.wasm", import.meta.url));
|
|
1564
|
+
const wasm = existsSync(bundled) ? bundled : createRequire(import.meta.url).resolve("tree-sitter-python/tree-sitter-python.wasm");
|
|
1565
|
+
const language = await Language.load(wasm);
|
|
1566
|
+
const p = new Parser();
|
|
1567
|
+
p.setLanguage(language);
|
|
1568
|
+
parser = p;
|
|
1569
|
+
}
|
|
1570
|
+
function pythonParserLoaded() {
|
|
1571
|
+
return parser !== void 0;
|
|
1572
|
+
}
|
|
1573
|
+
var HTTP_LIBS = /* @__PURE__ */ new Set(["requests", "httpx", "aiohttp"]);
|
|
1574
|
+
var VERBS2 = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
1575
|
+
function extractPython(source, file) {
|
|
1576
|
+
if (!parser) throw new Error("call loadPythonParser() before extracting Python");
|
|
1577
|
+
const tree = parser.parse(source);
|
|
1578
|
+
if (!tree) return [];
|
|
1579
|
+
try {
|
|
1580
|
+
const root = tree.rootNode;
|
|
1581
|
+
const ctx = { names: /* @__PURE__ */ new Map(), consts: /* @__PURE__ */ new Map() };
|
|
1582
|
+
collectImports2(root, ctx);
|
|
1583
|
+
collectConsts2(root, ctx);
|
|
1584
|
+
collectClients2(root, ctx);
|
|
1585
|
+
const sites = [];
|
|
1586
|
+
for (const call of root.descendantsOfType("call")) {
|
|
1587
|
+
if (!call) continue;
|
|
1588
|
+
const site = httpCall(ctx, call) ?? sdkCall(ctx, call);
|
|
1589
|
+
if (!site) continue;
|
|
1590
|
+
site.file = file;
|
|
1591
|
+
site.line = call.startPosition.row + 1;
|
|
1592
|
+
site.column = call.startPosition.column + 1;
|
|
1593
|
+
addResponseSurface2(call, site);
|
|
1594
|
+
sites.push(site);
|
|
1595
|
+
}
|
|
1596
|
+
return sites;
|
|
1597
|
+
} finally {
|
|
1598
|
+
tree.delete();
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
function collectImports2(root, ctx) {
|
|
1602
|
+
for (const st of root.descendantsOfType(["import_statement", "import_from_statement"])) {
|
|
1603
|
+
if (!st) continue;
|
|
1604
|
+
if (st.type === "import_statement") {
|
|
1605
|
+
for (const item of st.namedChildren) {
|
|
1606
|
+
if (!item) continue;
|
|
1607
|
+
const dotted = item.type === "aliased_import" ? item.childForFieldName("name") : item;
|
|
1608
|
+
const alias = item.type === "aliased_import" ? item.childForFieldName("alias")?.text : dotted?.text.split(".")[0];
|
|
1609
|
+
if (!dotted || !alias) continue;
|
|
1610
|
+
bindModule(ctx, alias, dotted.text);
|
|
1611
|
+
}
|
|
1612
|
+
} else {
|
|
1613
|
+
const module = st.childForFieldName("module_name")?.text;
|
|
1614
|
+
if (!module) continue;
|
|
1615
|
+
for (const item of st.childrenForFieldName("name")) {
|
|
1616
|
+
if (!item) continue;
|
|
1617
|
+
const name = item.type === "aliased_import" ? item.childForFieldName("name")?.text : item.text;
|
|
1618
|
+
const alias = item.type === "aliased_import" ? item.childForFieldName("alias")?.text : item.text;
|
|
1619
|
+
if (name && alias) bindModule(ctx, alias, `${module}.${name}`);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
function bindModule(ctx, alias, dotted) {
|
|
1625
|
+
const top = dotted.split(".")[0];
|
|
1626
|
+
if (HTTP_LIBS.has(top)) {
|
|
1627
|
+
ctx.names.set(alias, { kind: "http-module", lib: top });
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
const provider = providerForPythonModule(top);
|
|
1631
|
+
if (provider) ctx.names.set(alias, { kind: "sdk-module", provider, module: dotted });
|
|
1632
|
+
}
|
|
1633
|
+
function collectConsts2(root, ctx) {
|
|
1634
|
+
for (const st of root.namedChildren) {
|
|
1635
|
+
const assign = st?.type === "expression_statement" ? st.namedChildren[0] : void 0;
|
|
1636
|
+
if (assign?.type !== "assignment") continue;
|
|
1637
|
+
const left = assign.childForFieldName("left");
|
|
1638
|
+
const right = assign.childForFieldName("right");
|
|
1639
|
+
if (left?.type === "identifier" && right) ctx.consts.set(left.text, right);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
function collectClients2(root, ctx) {
|
|
1643
|
+
const visit = (target, value2) => {
|
|
1644
|
+
const call = unwrap2(value2);
|
|
1645
|
+
if (call.type !== "call") return;
|
|
1646
|
+
const fn = call.childForFieldName("function");
|
|
1647
|
+
const chain = fn ? chainOf2(fn) : void 0;
|
|
1648
|
+
if (!chain) return;
|
|
1649
|
+
const base = ctx.names.get(chain.root);
|
|
1650
|
+
if (!base) return;
|
|
1651
|
+
if (base.kind === "http-module" || base.kind === "http-client") {
|
|
1652
|
+
const ctor = chain.names.at(-1);
|
|
1653
|
+
if (!ctor || !/^(Session|Client|AsyncClient|ClientSession)$/.test(ctor)) return;
|
|
1654
|
+
const args = call.childForFieldName("arguments");
|
|
1655
|
+
const baseUrl = evalString2(ctx, keyword(args, "base_url"));
|
|
1656
|
+
ctx.names.set(target, { kind: "http-client", lib: base.lib, ...baseUrl ? { baseUrl } : {}, headers: dictEntries(ctx, keyword(args, "headers")) });
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
if (base.kind === "sdk-module" && !sdkEndpoint2(base.provider, [...modulePath(base), ...chain.names])) {
|
|
1660
|
+
ctx.names.set(target, { kind: "sdk-client", provider: base.provider, module: base.module });
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
for (const assign of root.descendantsOfType("assignment")) {
|
|
1664
|
+
const left = assign?.childForFieldName("left");
|
|
1665
|
+
const right = assign?.childForFieldName("right");
|
|
1666
|
+
if (left && right && (left.type === "identifier" || left.type === "attribute")) visit(left.text, right);
|
|
1667
|
+
}
|
|
1668
|
+
for (const item of root.descendantsOfType("as_pattern")) {
|
|
1669
|
+
const value2 = item?.namedChildren[0];
|
|
1670
|
+
const alias = item?.childForFieldName("alias")?.namedChildren[0] ?? item?.namedChildren.at(-1);
|
|
1671
|
+
if (value2 && alias && alias.type === "identifier") visit(alias.text, value2);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
function httpCall(ctx, call) {
|
|
1675
|
+
const fn = call.childForFieldName("function");
|
|
1676
|
+
const chain = fn ? chainOf2(fn) : void 0;
|
|
1677
|
+
if (!chain || chain.names.length === 0) {
|
|
1678
|
+
return void 0;
|
|
1679
|
+
}
|
|
1680
|
+
const binding = ctx.names.get(chain.root) ?? ctx.names.get(chain.prefix(chain.names.length - 1));
|
|
1681
|
+
const member = chain.names.at(-1);
|
|
1682
|
+
if (!binding || binding.kind !== "http-module" && binding.kind !== "http-client") return void 0;
|
|
1683
|
+
if (binding.kind === "http-module" && chain.names.length !== 1) return void 0;
|
|
1684
|
+
const args = call.childForFieldName("arguments");
|
|
1685
|
+
const positional = (args?.namedChildren ?? []).filter((a) => a && a.type !== "keyword_argument");
|
|
1686
|
+
let method;
|
|
1687
|
+
let urlNode;
|
|
1688
|
+
if (VERBS2.has(member)) {
|
|
1689
|
+
method = member.toUpperCase();
|
|
1690
|
+
urlNode = positional[0] ?? keyword(args, "url");
|
|
1691
|
+
} else if (member === "request") {
|
|
1692
|
+
const m = evalString2(ctx, positional[0] ?? keyword(args, "method"))?.toUpperCase();
|
|
1693
|
+
method = m && HTTP_METHODS.includes(m) ? m : null;
|
|
1694
|
+
urlNode = positional[1] ?? keyword(args, "url");
|
|
1695
|
+
} else return void 0;
|
|
1696
|
+
const base = binding.kind === "http-client" ? binding.baseUrl : void 0;
|
|
1697
|
+
const { host, path: path13, query } = parseUrl(joinUrl2(base, evalString2(ctx, urlNode)));
|
|
1698
|
+
const surface = emptySurface2();
|
|
1699
|
+
surface.headers = [...binding.kind === "http-client" ? binding.headers : [], ...dictEntries(ctx, keyword(args, "headers"))].map((h) => ({ ...h, name: h.name.toLowerCase() }));
|
|
1700
|
+
surface.query = [...query, ...dictEntries(ctx, keyword(args, "params"))];
|
|
1701
|
+
const provider = host ? providerForHost(host)?.id ?? null : null;
|
|
1702
|
+
return { file: "", line: 0, column: 0, provider, kind: "http", client: binding.lib, method, host, endpoint: path13, resolved: Boolean(provider && path13 && method), surface };
|
|
1703
|
+
}
|
|
1704
|
+
function sdkCall(ctx, call) {
|
|
1705
|
+
const fn = call.childForFieldName("function");
|
|
1706
|
+
const chain = fn ? chainOf2(fn) : void 0;
|
|
1707
|
+
if (!chain || chain.names.length === 0) return void 0;
|
|
1708
|
+
const binding = ctx.names.get(chain.root);
|
|
1709
|
+
if (!binding || binding.kind !== "sdk-module" && binding.kind !== "sdk-client") return void 0;
|
|
1710
|
+
const names = binding.kind === "sdk-module" ? [...modulePath(binding), ...chain.names] : chain.names;
|
|
1711
|
+
const ep = sdkEndpoint2(binding.provider, names);
|
|
1712
|
+
if (!ep && (binding.kind === "sdk-module" || names.length < 2)) return void 0;
|
|
1713
|
+
const surface = emptySurface2();
|
|
1714
|
+
const host = binding.provider.hosts.find((h) => !h.startsWith("*.")) ?? null;
|
|
1715
|
+
return {
|
|
1716
|
+
file: "",
|
|
1717
|
+
line: 0,
|
|
1718
|
+
column: 0,
|
|
1719
|
+
provider: binding.provider.id,
|
|
1720
|
+
kind: "sdk",
|
|
1721
|
+
client: binding.module.split(".")[0],
|
|
1722
|
+
operation: names.join("."),
|
|
1723
|
+
method: ep?.method ?? null,
|
|
1724
|
+
host,
|
|
1725
|
+
endpoint: ep?.path ?? null,
|
|
1726
|
+
resolved: Boolean(ep),
|
|
1727
|
+
surface
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
function modulePath(b) {
|
|
1731
|
+
return b.module.split(".").slice(1);
|
|
1732
|
+
}
|
|
1733
|
+
function sdkEndpoint2(provider, names) {
|
|
1734
|
+
let op = names.join(".");
|
|
1735
|
+
if (provider.id === "stripe") op = op.replace(/^v1\./, "");
|
|
1736
|
+
if (provider.id === "slack") {
|
|
1737
|
+
const last = names.at(-1);
|
|
1738
|
+
if (names.length !== 1 || !last || !/^[a-z]+(_[a-zA-Z0-9]+)+$/.test(last)) return void 0;
|
|
1739
|
+
if (last === "files_upload_v2") return { method: "POST", path: "/api/files.completeUploadExternal" };
|
|
1740
|
+
return { method: "POST", path: `/api/${last.replace(/_/g, ".")}` };
|
|
1741
|
+
}
|
|
1742
|
+
const ref = getProvider(provider.id)?.python?.methods?.[op];
|
|
1743
|
+
if (!ref) return void 0;
|
|
1744
|
+
const [method, path13] = ref.split(" ");
|
|
1745
|
+
return { method, path: path13 };
|
|
1746
|
+
}
|
|
1747
|
+
var NOT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
1748
|
+
"keys",
|
|
1749
|
+
"values",
|
|
1750
|
+
"items",
|
|
1751
|
+
"append",
|
|
1752
|
+
"extend",
|
|
1753
|
+
"lower",
|
|
1754
|
+
"upper",
|
|
1755
|
+
"strip",
|
|
1756
|
+
"split",
|
|
1757
|
+
"startswith",
|
|
1758
|
+
"endswith",
|
|
1759
|
+
"replace",
|
|
1760
|
+
"format",
|
|
1761
|
+
"encode",
|
|
1762
|
+
"decode",
|
|
1763
|
+
"count",
|
|
1764
|
+
"index",
|
|
1765
|
+
"copy",
|
|
1766
|
+
"pop",
|
|
1767
|
+
"update",
|
|
1768
|
+
"sort"
|
|
1769
|
+
]);
|
|
1770
|
+
var SAME_DATA = /* @__PURE__ */ new Set(["to_dict", "to_dict_recursive", "model_dump", "dict"]);
|
|
1771
|
+
var MAX_DEPTH2 = 4;
|
|
1772
|
+
var Collector2 = class {
|
|
1773
|
+
fields = /* @__PURE__ */ new Set();
|
|
1774
|
+
statusCodes = /* @__PURE__ */ new Set();
|
|
1775
|
+
checksOk = false;
|
|
1776
|
+
enums = /* @__PURE__ */ new Map();
|
|
1777
|
+
responseHeaders = /* @__PURE__ */ new Set();
|
|
1778
|
+
};
|
|
1779
|
+
function addResponseSurface2(call, site) {
|
|
1780
|
+
const c = new Collector2();
|
|
1781
|
+
const envelope = site.kind === "sdk" ? "body" : "response";
|
|
1782
|
+
bindFrom2(call, { space: envelope, path: [] }, site.client, c, 0);
|
|
1783
|
+
const s = site.surface;
|
|
1784
|
+
s.fields = [...c.fields].sort();
|
|
1785
|
+
s.statusCodes = [...c.statusCodes].sort((a, b) => a - b);
|
|
1786
|
+
s.checksOk = c.checksOk;
|
|
1787
|
+
s.enums = [...c.enums].map(([field, values]) => ({ field, values: [...values].sort() })).sort((a, b) => a.field.localeCompare(b.field));
|
|
1788
|
+
s.responseHeaders = [...c.responseHeaders].sort();
|
|
1789
|
+
}
|
|
1790
|
+
function bindFrom2(expr, loc, lib, c, depth) {
|
|
1791
|
+
if (depth > MAX_DEPTH2) return;
|
|
1792
|
+
let cur = expr;
|
|
1793
|
+
while (cur.parent && (cur.parent.type === "await" || cur.parent.type === "parenthesized_expression")) cur = cur.parent;
|
|
1794
|
+
const parent = cur.parent;
|
|
1795
|
+
if (!parent) return;
|
|
1796
|
+
if (parent.type === "assignment" && parent.childForFieldName("right")?.id === cur.id) {
|
|
1797
|
+
const left = parent.childForFieldName("left");
|
|
1798
|
+
if (left?.type === "identifier") bindName2(left.text, parent, loc, lib, c, depth);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
if (parent.type === "as_pattern" && parent.namedChildren[0]?.id === cur.id) {
|
|
1802
|
+
const alias = parent.childForFieldName("alias")?.namedChildren[0] ?? parent.namedChildren.at(-1);
|
|
1803
|
+
if (alias?.type === "identifier") bindName2(alias.text, parent, loc, lib, c, depth);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
use2(cur, loc, lib, c, depth);
|
|
1807
|
+
}
|
|
1808
|
+
function bindName2(name, decl, loc, lib, c, depth) {
|
|
1809
|
+
for (const ref of referencesOf2(name, decl)) use2(ref, loc, lib, c, depth);
|
|
1810
|
+
}
|
|
1811
|
+
function referencesOf2(name, decl) {
|
|
1812
|
+
let scope = decl.parent;
|
|
1813
|
+
while (scope && scope.type !== "function_definition" && scope.type !== "lambda" && scope.type !== "module") scope = scope.parent;
|
|
1814
|
+
if (!scope) return [];
|
|
1815
|
+
return scope.descendantsOfType("identifier").filter((id) => {
|
|
1816
|
+
if (!id || id.text !== name || id.startIndex < decl.endIndex) return false;
|
|
1817
|
+
const p = id.parent;
|
|
1818
|
+
if (p?.type === "attribute" && p.childForFieldName("attribute")?.id === id.id) return false;
|
|
1819
|
+
if (p?.type === "keyword_argument" && p.childForFieldName("name")?.id === id.id) return false;
|
|
1820
|
+
if (p?.type === "assignment" && p.childForFieldName("left")?.id === id.id) return false;
|
|
1821
|
+
return true;
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
function use2(ref, loc, lib, c, depth) {
|
|
1825
|
+
const access = [];
|
|
1826
|
+
let cur = ref;
|
|
1827
|
+
for (; ; ) {
|
|
1828
|
+
const p = cur.parent;
|
|
1829
|
+
if (!p) break;
|
|
1830
|
+
if (p.type === "attribute" && p.childForFieldName("object")?.id === cur.id) {
|
|
1831
|
+
const name = p.childForFieldName("attribute")?.text ?? "";
|
|
1832
|
+
const call = p.parent?.type === "call" && p.parent.childForFieldName("function")?.id === p.id ? p.parent : void 0;
|
|
1833
|
+
if (call && name === "get" && (loc.space === "body" || access.length > 0)) {
|
|
1834
|
+
const key = stringValue(call.childForFieldName("arguments")?.namedChildren[0] ?? void 0);
|
|
1835
|
+
if (key === void 0) break;
|
|
1836
|
+
access.push(key);
|
|
1837
|
+
cur = call;
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
if (call && SAME_DATA.has(name) && loc.space === "body") {
|
|
1841
|
+
cur = call;
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
access.push(name);
|
|
1845
|
+
cur = p;
|
|
1846
|
+
} else if (p.type === "subscript" && p.childForFieldName("value")?.id === cur.id) {
|
|
1847
|
+
const key = stringValue(p.childForFieldName("subscript") ?? void 0);
|
|
1848
|
+
access.push(key ?? "[]");
|
|
1849
|
+
cur = p;
|
|
1850
|
+
} else if (p.type === "parenthesized_expression" || p.type === "await") {
|
|
1851
|
+
cur = p;
|
|
1852
|
+
} else break;
|
|
1853
|
+
}
|
|
1854
|
+
interpret2(loc, access, cur, lib, c, depth);
|
|
1855
|
+
}
|
|
1856
|
+
function interpret2(loc, access, top, lib, c, depth) {
|
|
1857
|
+
const full = [...loc.path, ...access];
|
|
1858
|
+
if (loc.space === "body") return body2(full, top, lib, c, depth);
|
|
1859
|
+
const [first, ...rest] = full;
|
|
1860
|
+
if (first === void 0) return;
|
|
1861
|
+
const isCall = top.parent?.type === "call" && top.parent.childForFieldName("function")?.id === top.id;
|
|
1862
|
+
const statusKey = lib === "aiohttp" ? "status" : "status_code";
|
|
1863
|
+
if (first === statusKey && rest.length === 0) {
|
|
1864
|
+
for (const n of comparedLiterals2(top).nums) c.statusCodes.add(n);
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
if (first === "ok" && rest.length === 0 || first === "raise_for_status" && isCall) {
|
|
1868
|
+
c.checksOk = true;
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
if (first === "headers") {
|
|
1872
|
+
if (rest[0]) c.responseHeaders.add(rest[0].toLowerCase());
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
if (first === "json" && rest.length === 0 && isCall) bindFrom2(top.parent, { space: "body", path: [] }, lib, c, depth + 1);
|
|
1876
|
+
}
|
|
1877
|
+
function body2(path13, top, lib, c, depth) {
|
|
1878
|
+
const cut = path13.findIndex((p) => NOT_FIELDS2.has(p));
|
|
1879
|
+
const fieldPath = cut >= 0 ? path13.slice(0, cut) : path13;
|
|
1880
|
+
if (fieldPath.length === 0) {
|
|
1881
|
+
if (path13.length === 0 && depth < MAX_DEPTH2) iterate(top, { space: "body", path: [] }, lib, c, depth);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const field = fieldPath.join(".").replace(/\.\[\]/g, "[]");
|
|
1885
|
+
c.fields.add(field);
|
|
1886
|
+
if (cut < 0) {
|
|
1887
|
+
for (const s of comparedLiterals2(top).strs) {
|
|
1888
|
+
const set = c.enums.get(field) ?? /* @__PURE__ */ new Set();
|
|
1889
|
+
set.add(s);
|
|
1890
|
+
c.enums.set(field, set);
|
|
1891
|
+
}
|
|
1892
|
+
if (depth < MAX_DEPTH2) {
|
|
1893
|
+
const parent = top.parent;
|
|
1894
|
+
if (parent?.type === "assignment" && parent.childForFieldName("right")?.id === top.id) {
|
|
1895
|
+
const left = parent.childForFieldName("left");
|
|
1896
|
+
if (left?.type === "identifier") bindName2(left.text, parent, { space: "body", path: fieldPath }, lib, c, depth + 1);
|
|
1897
|
+
}
|
|
1898
|
+
iterate(top, { space: "body", path: fieldPath }, lib, c, depth);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
function iterate(top, loc, lib, c, depth) {
|
|
1903
|
+
const p = top.parent;
|
|
1904
|
+
const items = { space: "body", path: [...loc.path, "[]"] };
|
|
1905
|
+
if (p?.type === "for_statement" && p.childForFieldName("right")?.id === top.id) {
|
|
1906
|
+
const left = p.childForFieldName("left");
|
|
1907
|
+
if (left?.type === "identifier") bindName2(left.text, left, items, lib, c, depth + 1);
|
|
1908
|
+
} else if (p?.type === "for_in_clause" && p.childForFieldName("right")?.id === top.id) {
|
|
1909
|
+
const left = p.childForFieldName("left");
|
|
1910
|
+
const comp = p.parent;
|
|
1911
|
+
if (left?.type === "identifier" && comp) {
|
|
1912
|
+
for (const id of comp.descendantsOfType("identifier")) {
|
|
1913
|
+
if (id && id.text === left.text && id.id !== left.id) use2(id, items, lib, c, depth + 1);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
function comparedLiterals2(node) {
|
|
1919
|
+
const nums = [];
|
|
1920
|
+
const strs = [];
|
|
1921
|
+
let cur = node;
|
|
1922
|
+
while (cur.parent?.type === "parenthesized_expression") cur = cur.parent;
|
|
1923
|
+
const p = cur.parent;
|
|
1924
|
+
if (p?.type !== "comparison_operator") return { nums, strs };
|
|
1925
|
+
const take = (n) => {
|
|
1926
|
+
if (!n) return;
|
|
1927
|
+
if (n.type === "integer") nums.push(Number(n.text));
|
|
1928
|
+
else if (n.type === "string") {
|
|
1929
|
+
const v = stringValue(n);
|
|
1930
|
+
if (v !== void 0) strs.push(v);
|
|
1931
|
+
} else if (n.type === "tuple" || n.type === "list" || n.type === "set") n.namedChildren.forEach(take);
|
|
1932
|
+
};
|
|
1933
|
+
for (const operand of p.namedChildren) if (operand && operand.id !== cur.id) take(operand);
|
|
1934
|
+
return { nums, strs };
|
|
1935
|
+
}
|
|
1936
|
+
function chainOf2(node) {
|
|
1937
|
+
const names = [];
|
|
1938
|
+
let cur = node;
|
|
1939
|
+
while (cur.type === "attribute") {
|
|
1940
|
+
names.unshift(cur.childForFieldName("attribute").text);
|
|
1941
|
+
cur = cur.childForFieldName("object");
|
|
1942
|
+
}
|
|
1943
|
+
if (cur.type !== "identifier") return void 0;
|
|
1944
|
+
const root = cur.text;
|
|
1945
|
+
return { root, names, prefix: (n) => [root, ...names.slice(0, n)].join(".") };
|
|
1946
|
+
}
|
|
1947
|
+
function unwrap2(n) {
|
|
1948
|
+
let cur = n;
|
|
1949
|
+
while (cur.type === "await" || cur.type === "parenthesized_expression") cur = cur.namedChildren[0] ?? cur;
|
|
1950
|
+
return cur;
|
|
1951
|
+
}
|
|
1952
|
+
function keyword(args, name) {
|
|
1953
|
+
for (const a of args?.namedChildren ?? []) {
|
|
1954
|
+
if (a?.type === "keyword_argument" && a.childForFieldName("name")?.text === name) return a.childForFieldName("value") ?? void 0;
|
|
1955
|
+
}
|
|
1956
|
+
return void 0;
|
|
1957
|
+
}
|
|
1958
|
+
function stringValue(n) {
|
|
1959
|
+
if (!n || n.type !== "string") return void 0;
|
|
1960
|
+
if (n.namedChildren.some((c) => c?.type === "interpolation")) return void 0;
|
|
1961
|
+
return n.namedChildren.filter((c) => c?.type === "string_content" || c?.type === "escape_sequence").map((c) => c.text).join("");
|
|
1962
|
+
}
|
|
1963
|
+
function evalString2(ctx, node, depth = 0) {
|
|
1964
|
+
if (!node || depth > 8) return void 0;
|
|
1965
|
+
const n = unwrap2(node);
|
|
1966
|
+
if (n.type === "string") {
|
|
1967
|
+
let out = "";
|
|
1968
|
+
for (const part of n.namedChildren) {
|
|
1969
|
+
if (!part) continue;
|
|
1970
|
+
if (part.type === "string_content" || part.type === "escape_sequence") out += part.text;
|
|
1971
|
+
else if (part.type === "interpolation") {
|
|
1972
|
+
const inner = part.childForFieldName("expression") ?? part.namedChildren[0];
|
|
1973
|
+
out += (inner && evalString2(ctx, inner, depth + 1)) ?? placeholder2(inner ?? void 0);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
return out;
|
|
1977
|
+
}
|
|
1978
|
+
if (n.type === "concatenated_string") return n.namedChildren.map((s) => evalString2(ctx, s ?? void 0, depth + 1) ?? "").join("");
|
|
1979
|
+
if (n.type === "binary_operator" && n.childForFieldName("operator")?.text === "+") {
|
|
1980
|
+
const l = evalString2(ctx, n.childForFieldName("left") ?? void 0, depth + 1);
|
|
1981
|
+
const r = evalString2(ctx, n.childForFieldName("right") ?? void 0, depth + 1);
|
|
1982
|
+
if (l === void 0 && r === void 0) return void 0;
|
|
1983
|
+
return (l ?? placeholder2(n.childForFieldName("left") ?? void 0)) + (r ?? placeholder2(n.childForFieldName("right") ?? void 0));
|
|
1984
|
+
}
|
|
1985
|
+
if (n.type === "identifier") {
|
|
1986
|
+
const init = ctx.consts.get(n.text);
|
|
1987
|
+
return init && init.id !== node.id ? evalString2(ctx, init, depth + 1) : void 0;
|
|
1988
|
+
}
|
|
1989
|
+
if (n.type === "call") {
|
|
1990
|
+
const fn = n.childForFieldName("function")?.text;
|
|
1991
|
+
const args = (n.childForFieldName("arguments")?.namedChildren ?? []).filter(Boolean);
|
|
1992
|
+
if ((fn === "os.getenv" || fn === "getenv" || fn === "os.environ.get") && args[1]) return evalString2(ctx, args[1], depth + 1);
|
|
1993
|
+
}
|
|
1994
|
+
return void 0;
|
|
1995
|
+
}
|
|
1996
|
+
function placeholder2(n) {
|
|
1997
|
+
if (!n) return "{}";
|
|
1998
|
+
const e = unwrap2(n);
|
|
1999
|
+
if (e.type === "identifier") return `{${e.text}}`;
|
|
2000
|
+
if (e.type === "attribute") return `{${e.childForFieldName("attribute")?.text ?? ""}}`;
|
|
2001
|
+
const args = e.type === "call" ? (e.childForFieldName("arguments")?.namedChildren ?? []).filter(Boolean) : [];
|
|
2002
|
+
if (args.length === 1) return placeholder2(args[0]);
|
|
2003
|
+
return "{}";
|
|
2004
|
+
}
|
|
2005
|
+
function dictEntries(ctx, node, depth = 0) {
|
|
2006
|
+
if (!node || depth > 3) return [];
|
|
2007
|
+
const n = unwrap2(node);
|
|
2008
|
+
if (n.type === "identifier") {
|
|
2009
|
+
const init = ctx.consts.get(n.text);
|
|
2010
|
+
return init ? dictEntries(ctx, init, depth + 1) : [];
|
|
2011
|
+
}
|
|
2012
|
+
if (n.type === "call" && n.childForFieldName("function")?.text === "dict") {
|
|
2013
|
+
return (n.childForFieldName("arguments")?.namedChildren ?? []).filter((a) => a?.type === "keyword_argument").map((a) => ({ name: a.childForFieldName("name").text, value: literal(ctx, a.childForFieldName("value") ?? void 0) }));
|
|
2014
|
+
}
|
|
2015
|
+
if (n.type !== "dictionary") return [];
|
|
2016
|
+
const out = [];
|
|
2017
|
+
for (const pair of n.namedChildren) {
|
|
2018
|
+
if (pair?.type !== "pair") continue;
|
|
2019
|
+
const key = stringValue(pair.childForFieldName("key") ?? void 0);
|
|
2020
|
+
if (key !== void 0) out.push({ name: key, value: literal(ctx, pair.childForFieldName("value") ?? void 0) });
|
|
2021
|
+
}
|
|
2022
|
+
return out;
|
|
2023
|
+
}
|
|
2024
|
+
function literal(ctx, n) {
|
|
2025
|
+
if (!n) return null;
|
|
2026
|
+
if (n.type === "integer" || n.type === "float") return n.text;
|
|
2027
|
+
if (n.type === "true" || n.type === "false") return n.text.toLowerCase();
|
|
2028
|
+
return evalString2(ctx, n) ?? null;
|
|
2029
|
+
}
|
|
2030
|
+
function joinUrl2(base, url) {
|
|
2031
|
+
if (url === void 0) return base;
|
|
2032
|
+
if (!base || /^[a-z]+:\/\//i.test(url)) return url;
|
|
2033
|
+
return base.replace(/\/+$/, "") + "/" + url.replace(/^\/+/, "");
|
|
2034
|
+
}
|
|
2035
|
+
function emptySurface2() {
|
|
2036
|
+
return { fields: [], statusCodes: [], checksOk: false, enums: [], headers: [], query: [], responseHeaders: [] };
|
|
2037
|
+
}
|
|
1369
2038
|
|
|
1370
2039
|
// packages/extractor/src/index.ts
|
|
1371
2040
|
var SCRIPT_KINDS = {
|
|
@@ -1380,7 +2049,7 @@ var SCRIPT_KINDS = {
|
|
|
1380
2049
|
};
|
|
1381
2050
|
function extractCallSites(rootDir) {
|
|
1382
2051
|
const root = path4.resolve(rootDir);
|
|
1383
|
-
const { sources, manifests } = listRepoFiles(root);
|
|
2052
|
+
const { sources, manifests, python, pythonManifests } = listRepoFiles(root);
|
|
1384
2053
|
const project = new Project({
|
|
1385
2054
|
useInMemoryFileSystem: true,
|
|
1386
2055
|
skipFileDependencyResolution: true,
|
|
@@ -1399,12 +2068,28 @@ function extractCallSites(rootDir) {
|
|
|
1399
2068
|
}
|
|
1400
2069
|
}
|
|
1401
2070
|
found.sort((a, b) => a.site.file.localeCompare(b.site.file) || a.site.line - b.site.line || a.site.column - b.site.column);
|
|
2071
|
+
const skipped = [];
|
|
2072
|
+
const pythonSites = [];
|
|
2073
|
+
for (const abs of python) {
|
|
2074
|
+
const rel = toPosix(path4.relative(root, abs));
|
|
2075
|
+
if (!pythonParserLoaded()) {
|
|
2076
|
+
skipped.push({ file: rel, reason: "the Python parser was not loaded" });
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
try {
|
|
2080
|
+
pythonSites.push(...extractPython(readFileSync3(abs, "utf8"), rel));
|
|
2081
|
+
} catch (err) {
|
|
2082
|
+
skipped.push({ file: rel, reason: err instanceof Error ? err.message : String(err) });
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
const callSites = [...found.map((f) => f.site), ...pythonSites].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
|
|
1402
2086
|
return {
|
|
1403
2087
|
root,
|
|
1404
|
-
filesScanned: sources.length,
|
|
1405
|
-
callSites
|
|
1406
|
-
declared: declaredProviders(root, manifests),
|
|
1407
|
-
found
|
|
2088
|
+
filesScanned: sources.length + python.length - skipped.length,
|
|
2089
|
+
callSites,
|
|
2090
|
+
declared: [...declaredProviders(root, manifests), ...declaredPythonProviders(root, pythonManifests)],
|
|
2091
|
+
found,
|
|
2092
|
+
skipped
|
|
1408
2093
|
};
|
|
1409
2094
|
}
|
|
1410
2095
|
|
|
@@ -1520,9 +2205,9 @@ function inferShape(res, opts) {
|
|
|
1520
2205
|
else if (!VOLATILE_HEADERS.has(name) && !name.startsWith("x-ratelimit") && !name.startsWith("ratelimit")) headerNames.push(name);
|
|
1521
2206
|
}
|
|
1522
2207
|
const text = res.body ?? "";
|
|
1523
|
-
let
|
|
2208
|
+
let body3;
|
|
1524
2209
|
if (text.trim() === "") {
|
|
1525
|
-
|
|
2210
|
+
body3 = { kind: "empty", fields: [] };
|
|
1526
2211
|
} else {
|
|
1527
2212
|
let parsed;
|
|
1528
2213
|
let isJson = true;
|
|
@@ -1534,16 +2219,16 @@ function inferShape(res, opts) {
|
|
|
1534
2219
|
if (isJson) {
|
|
1535
2220
|
const acc = /* @__PURE__ */ new Map();
|
|
1536
2221
|
walk(parsed, "", acc);
|
|
1537
|
-
|
|
2222
|
+
body3 = { kind: "json", rootType: typeOf(parsed), fields: finish(acc, opts.salt) };
|
|
1538
2223
|
} else {
|
|
1539
|
-
|
|
2224
|
+
body3 = { kind: "text", fields: [] };
|
|
1540
2225
|
}
|
|
1541
2226
|
}
|
|
1542
2227
|
return {
|
|
1543
2228
|
status: res.status,
|
|
1544
2229
|
headers: Object.fromEntries(Object.entries(headers).sort(([a], [b]) => cmp3(a, b))),
|
|
1545
2230
|
headerNames: [...new Set(headerNames)].sort(),
|
|
1546
|
-
body:
|
|
2231
|
+
body: body3
|
|
1547
2232
|
};
|
|
1548
2233
|
}
|
|
1549
2234
|
function walk(value2, path13, acc) {
|
|
@@ -1939,8 +2624,181 @@ function replay(recording, store, secret) {
|
|
|
1939
2624
|
return { recording, observations, after, before, ...before ? { diff: diffShapes(before.shape, after.shape) } : {} };
|
|
1940
2625
|
}
|
|
1941
2626
|
|
|
1942
|
-
// packages/repair/src/recipes/
|
|
2627
|
+
// packages/repair/src/recipes/apply-fix.ts
|
|
1943
2628
|
import { Node as Node3, SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
2629
|
+
var applyFix = {
|
|
2630
|
+
id: "apply-fix",
|
|
2631
|
+
changeClass: "version-sunset",
|
|
2632
|
+
description: "Apply the advisory's fix: the new path, query value or header, and the renamed fields and enum values",
|
|
2633
|
+
edits({ sourceFile, call, advisory }) {
|
|
2634
|
+
const fix2 = advisory.fix;
|
|
2635
|
+
if (!fix2) return [];
|
|
2636
|
+
const where = (n) => `${sourceFile.getFilePath().slice(1)}:${n.getStartLineNumber()}`;
|
|
2637
|
+
const edits = [...requestEdits(call, sourceFile, fix2, where), ...headerEdits(call, fix2, where)];
|
|
2638
|
+
const scope = enclosingFunction(call) ?? sourceFile;
|
|
2639
|
+
edits.push(...fieldEdits(scope, fix2, where, edits), ...enumEdits(scope, fix2, where));
|
|
2640
|
+
return edits;
|
|
2641
|
+
}
|
|
2642
|
+
};
|
|
2643
|
+
var TEXT_KINDS = [SyntaxKind3.StringLiteral, SyntaxKind3.NoSubstitutionTemplateLiteral, SyntaxKind3.TemplateHead, SyntaxKind3.TemplateMiddle, SyntaxKind3.TemplateTail];
|
|
2644
|
+
function requestEdits(call, sf, fix2, where) {
|
|
2645
|
+
if (!fix2.path && !fix2.query) return [];
|
|
2646
|
+
const edits = [];
|
|
2647
|
+
for (const node of urlTextNodes(call, sf)) {
|
|
2648
|
+
const before = node.getText();
|
|
2649
|
+
let after = before;
|
|
2650
|
+
if (fix2.path) {
|
|
2651
|
+
after = after.split(fix2.path.from).join(fix2.path.to);
|
|
2652
|
+
const from = fix2.path.from.replace(/\/$/, "");
|
|
2653
|
+
const to = fix2.path.to.replace(/\/$/, "");
|
|
2654
|
+
if (from !== fix2.path.from) after = after.replace(new RegExp(`${escapeRe(from)}(?=["'\`]|\\$\\{)`, "g"), to);
|
|
2655
|
+
}
|
|
2656
|
+
if (fix2.query) {
|
|
2657
|
+
const re = new RegExp(`([?&])${escapeRe(fix2.query.name)}=([^&#\`'"$]+)`, "g");
|
|
2658
|
+
after = after.replace(re, (_m, sep) => `${sep}${fix2.query.name}=${fix2.query.value}`);
|
|
2659
|
+
}
|
|
2660
|
+
if (after !== before) edits.push({ start: node.getStart(), end: node.getEnd(), text: after, note: `${where(node)} \`${before}\` \u2192 \`${after}\`` });
|
|
2661
|
+
}
|
|
2662
|
+
if (fix2.query) {
|
|
2663
|
+
for (const prop of [...objectProperties(call, "params"), ...objectProperties(call, "searchParams")]) {
|
|
2664
|
+
if (propName2(prop) !== fix2.query.name || !Node3.isPropertyAssignment(prop)) continue;
|
|
2665
|
+
const init = prop.getInitializer();
|
|
2666
|
+
if (!init || !(Node3.isNumericLiteral(init) || Node3.isStringLiteral(init))) continue;
|
|
2667
|
+
const text = /^\d+(\.\d+)?$/.test(fix2.query.value) && Node3.isNumericLiteral(init) ? fix2.query.value : JSON.stringify(fix2.query.value);
|
|
2668
|
+
if (init.getText() !== text) edits.push({ start: init.getStart(), end: init.getEnd(), text, note: `${where(init)} ${fix2.query.name}: \`${init.getText()}\` \u2192 \`${text}\`` });
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
return edits;
|
|
2672
|
+
}
|
|
2673
|
+
function headerEdits(call, fix2, where) {
|
|
2674
|
+
if (!fix2.header) return [];
|
|
2675
|
+
const edits = [];
|
|
2676
|
+
const text = JSON.stringify(fix2.header.value);
|
|
2677
|
+
for (const prop of objectProperties(call, "headers")) {
|
|
2678
|
+
if (propName2(prop)?.toLowerCase() !== fix2.header.name.toLowerCase() || !Node3.isPropertyAssignment(prop)) continue;
|
|
2679
|
+
const init = prop.getInitializer();
|
|
2680
|
+
if (!init || !(Node3.isStringLiteral(init) || Node3.isNoSubstitutionTemplateLiteral(init)) || init.getLiteralText() === fix2.header.value) continue;
|
|
2681
|
+
edits.push({ start: init.getStart(), end: init.getEnd(), text, note: `${where(init)} ${fix2.header.name}: \`${init.getText()}\` \u2192 \`${text}\`` });
|
|
2682
|
+
}
|
|
2683
|
+
return edits;
|
|
2684
|
+
}
|
|
2685
|
+
function fieldEdits(scope, fix2, where, taken) {
|
|
2686
|
+
const edits = [];
|
|
2687
|
+
for (const { from, to } of fix2.fields ?? []) {
|
|
2688
|
+
const oldName = leaf(from);
|
|
2689
|
+
const newName = leaf(to);
|
|
2690
|
+
if (oldName === newName) continue;
|
|
2691
|
+
for (const access of scope.getDescendantsOfKind(SyntaxKind3.PropertyAccessExpression)) {
|
|
2692
|
+
if (access.getName() !== oldName || isWrite(access) || overlaps(access, [...taken, ...edits])) continue;
|
|
2693
|
+
const target = access.getExpression().getText();
|
|
2694
|
+
const dot = access.hasQuestionDotToken() ? "?." : ".";
|
|
2695
|
+
const text = `(${target}${dot}${newName} ?? ${target}${dot}${oldName})`;
|
|
2696
|
+
edits.push({ start: access.getStart(), end: access.getEnd(), text, note: `${where(access)} \`${access.getText()}\` \u2192 \`${text}\`` });
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
return edits;
|
|
2700
|
+
}
|
|
2701
|
+
function enumEdits(scope, fix2, where) {
|
|
2702
|
+
const edits = [];
|
|
2703
|
+
for (const { field, values } of fix2.enums ?? []) {
|
|
2704
|
+
const name = leaf(field);
|
|
2705
|
+
const renamed = new Map(values.map((v) => [v.from, v.to]));
|
|
2706
|
+
const isField = (n) => Node3.isPropertyAccessExpression(n) && n.getName() === name || Node3.isIdentifier(n) && n.getText() === name;
|
|
2707
|
+
for (const bin of scope.getDescendantsOfKind(SyntaxKind3.BinaryExpression)) {
|
|
2708
|
+
const op = bin.getOperatorToken().getKind();
|
|
2709
|
+
const negated = op === SyntaxKind3.ExclamationEqualsEqualsToken || op === SyntaxKind3.ExclamationEqualsToken;
|
|
2710
|
+
if (!negated && op !== SyntaxKind3.EqualsEqualsEqualsToken && op !== SyntaxKind3.EqualsEqualsToken) continue;
|
|
2711
|
+
const [lit, other] = Node3.isStringLiteral(bin.getRight()) ? [bin.getRight(), bin.getLeft()] : [bin.getLeft(), bin.getRight()];
|
|
2712
|
+
if (!Node3.isStringLiteral(lit) || !isField(other)) continue;
|
|
2713
|
+
const to = renamed.get(lit.getLiteralText());
|
|
2714
|
+
if (to === void 0) continue;
|
|
2715
|
+
const text = `${negated ? "!" : ""}${JSON.stringify([lit.getLiteralText(), to])}.includes(${other.getText()})`;
|
|
2716
|
+
edits.push({ start: bin.getStart(), end: bin.getEnd(), text, note: `${where(bin)} \`${bin.getText()}\` \u2192 \`${text}\`` });
|
|
2717
|
+
}
|
|
2718
|
+
for (const sw of scope.getDescendantsOfKind(SyntaxKind3.SwitchStatement)) {
|
|
2719
|
+
if (!isField(sw.getExpression())) continue;
|
|
2720
|
+
const cases = sw.getClauses().filter(Node3.isCaseClause);
|
|
2721
|
+
const present = new Set(cases.map((c) => c.getExpression()).filter(Node3.isStringLiteral).map((l) => l.getLiteralText()));
|
|
2722
|
+
for (const c of cases) {
|
|
2723
|
+
const e = c.getExpression();
|
|
2724
|
+
const to = Node3.isStringLiteral(e) ? renamed.get(e.getLiteralText()) : void 0;
|
|
2725
|
+
if (to === void 0 || present.has(to)) continue;
|
|
2726
|
+
present.add(to);
|
|
2727
|
+
const indent = " ".repeat(c.getSourceFile().getLineAndColumnAtPos(c.getStart()).column - 1);
|
|
2728
|
+
edits.push({ start: c.getStart(), end: c.getStart(), text: `case ${JSON.stringify(to)}:
|
|
2729
|
+
${indent}`, note: `${where(c)} add case ${JSON.stringify(to)}` });
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
for (const inc of scope.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
|
|
2733
|
+
const callee = inc.getExpression();
|
|
2734
|
+
if (!Node3.isPropertyAccessExpression(callee) || callee.getName() !== "includes" || !isField(inc.getArguments()[0] ?? inc)) continue;
|
|
2735
|
+
const arr = callee.getExpression();
|
|
2736
|
+
if (!Node3.isArrayLiteralExpression(arr)) continue;
|
|
2737
|
+
const items = arr.getElements().filter(Node3.isStringLiteral).map((l) => l.getLiteralText());
|
|
2738
|
+
const add = items.map((v) => renamed.get(v)).filter((v) => v !== void 0 && !items.includes(v));
|
|
2739
|
+
if (!add.length) continue;
|
|
2740
|
+
const text = JSON.stringify([...items, ...new Set(add)]);
|
|
2741
|
+
edits.push({ start: arr.getStart(), end: arr.getEnd(), text, note: `${where(arr)} \`${arr.getText()}\` \u2192 \`${text}\`` });
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
return edits;
|
|
2745
|
+
}
|
|
2746
|
+
function urlTextNodes(call, sf) {
|
|
2747
|
+
const out = /* @__PURE__ */ new Set();
|
|
2748
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2749
|
+
const visit = (n, depth) => {
|
|
2750
|
+
if (!n || depth > 4 || seen.has(n)) return;
|
|
2751
|
+
seen.add(n);
|
|
2752
|
+
if (TEXT_KINDS.includes(n.getKind())) out.add(n);
|
|
2753
|
+
if (Node3.isIdentifier(n)) {
|
|
2754
|
+
const decl = sf.getVariableDeclaration(n.getText());
|
|
2755
|
+
if (decl?.getVariableStatement()?.getDeclarationKind() === "const") visit(decl.getInitializer(), depth + 1);
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
n.forEachChild((c) => visit(c, depth));
|
|
2759
|
+
};
|
|
2760
|
+
const [first] = call.getArguments();
|
|
2761
|
+
if (first && Node3.isObjectLiteralExpression(first)) {
|
|
2762
|
+
const url = first.getProperty("url");
|
|
2763
|
+
if (url && Node3.isPropertyAssignment(url)) visit(url.getInitializer(), 0);
|
|
2764
|
+
} else visit(first, 0);
|
|
2765
|
+
for (const prop of sf.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) if (propName2(prop) === "baseURL") visit(prop.getInitializer(), 0);
|
|
2766
|
+
return [...out];
|
|
2767
|
+
}
|
|
2768
|
+
function objectProperties(call, name) {
|
|
2769
|
+
const out = [];
|
|
2770
|
+
for (const arg of call.getArguments()) {
|
|
2771
|
+
if (!Node3.isObjectLiteralExpression(arg)) continue;
|
|
2772
|
+
const prop = arg.getProperties().find((p) => propName2(p) === name);
|
|
2773
|
+
const value2 = prop && Node3.isPropertyAssignment(prop) ? prop.getInitializer() : void 0;
|
|
2774
|
+
if (value2 && Node3.isObjectLiteralExpression(value2)) out.push(...value2.getProperties());
|
|
2775
|
+
}
|
|
2776
|
+
return out;
|
|
2777
|
+
}
|
|
2778
|
+
function propName2(p) {
|
|
2779
|
+
if (!(Node3.isPropertyAssignment(p) || Node3.isShorthandPropertyAssignment(p))) return void 0;
|
|
2780
|
+
const n = p.getNameNode();
|
|
2781
|
+
return Node3.isStringLiteral(n) || Node3.isNoSubstitutionTemplateLiteral(n) ? n.getLiteralText() : n.getText();
|
|
2782
|
+
}
|
|
2783
|
+
function enclosingFunction(call) {
|
|
2784
|
+
return call.getFirstAncestor((n) => Node3.isFunctionDeclaration(n) || Node3.isArrowFunction(n) || Node3.isFunctionExpression(n) || Node3.isMethodDeclaration(n));
|
|
2785
|
+
}
|
|
2786
|
+
function isWrite(access) {
|
|
2787
|
+
const p = access.getParent();
|
|
2788
|
+
return Node3.isBinaryExpression(p) && p.getLeft() === access && p.getOperatorToken().getKind() === SyntaxKind3.EqualsToken;
|
|
2789
|
+
}
|
|
2790
|
+
function overlaps(n, edits) {
|
|
2791
|
+
return edits.some((e) => n.getStart() < e.end && e.start < n.getEnd());
|
|
2792
|
+
}
|
|
2793
|
+
function leaf(path13) {
|
|
2794
|
+
return path13.split(".").pop().replace(/\[\]/g, "");
|
|
2795
|
+
}
|
|
2796
|
+
function escapeRe(s) {
|
|
2797
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
// packages/repair/src/recipes/status-code-accept-set.ts
|
|
2801
|
+
import { Node as Node4, SyntaxKind as SyntaxKind4 } from "ts-morph";
|
|
1944
2802
|
var STATUS_PROPS = /* @__PURE__ */ new Set(["status", "statusCode"]);
|
|
1945
2803
|
var statusCodeAcceptSet = {
|
|
1946
2804
|
id: "status-code-accept-set",
|
|
@@ -1955,23 +2813,23 @@ var statusCodeAcceptSet = {
|
|
|
1955
2813
|
const where = (n) => `${sourceFile.getFilePath().slice(1)}:${n.getStartLineNumber()}`;
|
|
1956
2814
|
for (const status of statusExpressions(call)) {
|
|
1957
2815
|
let cur = status;
|
|
1958
|
-
while (
|
|
2816
|
+
while (Node4.isParenthesizedExpression(cur.getParent())) cur = cur.getParent();
|
|
1959
2817
|
const parent = cur.getParent();
|
|
1960
|
-
if (
|
|
2818
|
+
if (Node4.isBinaryExpression(parent)) {
|
|
1961
2819
|
const op = parent.getOperatorToken().getKind();
|
|
1962
2820
|
const other = parent.getLeft() === cur ? parent.getRight() : parent.getLeft();
|
|
1963
|
-
if (!
|
|
1964
|
-
const negated = op ===
|
|
1965
|
-
const equal = op ===
|
|
2821
|
+
if (!Node4.isNumericLiteral(other) || !oldCodes.includes(Number(other.getLiteralValue()))) continue;
|
|
2822
|
+
const negated = op === SyntaxKind4.ExclamationEqualsEqualsToken || op === SyntaxKind4.ExclamationEqualsToken;
|
|
2823
|
+
const equal = op === SyntaxKind4.EqualsEqualsEqualsToken || op === SyntaxKind4.EqualsEqualsToken;
|
|
1966
2824
|
if (!negated && !equal) continue;
|
|
1967
2825
|
const text = `${negated ? "!" : ""}${list}.includes(${status.getText()})`;
|
|
1968
2826
|
edits.push({ start: parent.getStart(), end: parent.getEnd(), text, note: `${where(parent)} \`${parent.getText()}\` \u2192 \`${text}\`` });
|
|
1969
|
-
} else if (
|
|
1970
|
-
const cases = parent.getClauses().filter(
|
|
1971
|
-
const present = new Set(cases.map((c) => c.getExpression()).filter(
|
|
2827
|
+
} else if (Node4.isSwitchStatement(parent) && parent.getExpression() === cur) {
|
|
2828
|
+
const cases = parent.getClauses().filter(Node4.isCaseClause);
|
|
2829
|
+
const present = new Set(cases.map((c) => c.getExpression()).filter(Node4.isNumericLiteral).map((l) => Number(l.getLiteralValue())));
|
|
1972
2830
|
const anchor = cases.find((c) => {
|
|
1973
2831
|
const e = c.getExpression();
|
|
1974
|
-
return
|
|
2832
|
+
return Node4.isNumericLiteral(e) && oldCodes.includes(Number(e.getLiteralValue()));
|
|
1975
2833
|
});
|
|
1976
2834
|
const missing = accept.filter((c) => !present.has(c));
|
|
1977
2835
|
if (!anchor || missing.length === 0) continue;
|
|
@@ -1979,11 +2837,11 @@ var statusCodeAcceptSet = {
|
|
|
1979
2837
|
const text = missing.map((c) => `case ${c}:
|
|
1980
2838
|
${indent}`).join("");
|
|
1981
2839
|
edits.push({ start: anchor.getStart(), end: anchor.getStart(), text, note: `${where(anchor)} add ${missing.map((c) => `case ${c}`).join(", ")}` });
|
|
1982
|
-
} else if (
|
|
2840
|
+
} else if (Node4.isCallExpression(parent) && parent.getArguments()[0] === cur) {
|
|
1983
2841
|
const callee = parent.getExpression();
|
|
1984
|
-
const arr =
|
|
1985
|
-
if (!arr || !
|
|
1986
|
-
const values = arr.getElements().filter(
|
|
2842
|
+
const arr = Node4.isPropertyAccessExpression(callee) && callee.getName() === "includes" ? callee.getExpression() : void 0;
|
|
2843
|
+
if (!arr || !Node4.isArrayLiteralExpression(arr)) continue;
|
|
2844
|
+
const values = arr.getElements().filter(Node4.isNumericLiteral).map((l) => Number(l.getLiteralValue()));
|
|
1987
2845
|
if (!values.some((v) => oldCodes.includes(v))) continue;
|
|
1988
2846
|
const merged = [.../* @__PURE__ */ new Set([...values, ...accept])].sort((a, b) => a - b);
|
|
1989
2847
|
if (merged.length === values.length) continue;
|
|
@@ -1999,52 +2857,52 @@ function statusExpressions(call) {
|
|
|
1999
2857
|
let cur = call;
|
|
2000
2858
|
for (; ; ) {
|
|
2001
2859
|
const p = cur.getParent();
|
|
2002
|
-
if (p && (
|
|
2860
|
+
if (p && (Node4.isAwaitExpression(p) || Node4.isParenthesizedExpression(p) || Node4.isAsExpression(p) || Node4.isNonNullExpression(p))) cur = p;
|
|
2003
2861
|
else break;
|
|
2004
2862
|
}
|
|
2005
2863
|
const parent = cur.getParent();
|
|
2006
2864
|
const sf = call.getSourceFile();
|
|
2007
|
-
if (
|
|
2008
|
-
if (
|
|
2009
|
-
const cb =
|
|
2010
|
-
const param = cb && (
|
|
2865
|
+
if (Node4.isPropertyAccessExpression(parent) && parent.getExpression() === cur && STATUS_PROPS.has(parent.getName())) out.push(parent);
|
|
2866
|
+
if (Node4.isPropertyAccessExpression(parent) && parent.getName() === "then") {
|
|
2867
|
+
const cb = Node4.isCallExpression(parent.getParent()) ? parent.getParent().getArguments()[0] : void 0;
|
|
2868
|
+
const param = cb && (Node4.isArrowFunction(cb) || Node4.isFunctionExpression(cb)) ? cb.getParameters()[0] : void 0;
|
|
2011
2869
|
if (param) out.push(...fromBinding(param.getNameNode(), param, sf));
|
|
2012
2870
|
}
|
|
2013
|
-
if (
|
|
2871
|
+
if (Node4.isVariableDeclaration(parent) && parent.getInitializer() === cur) out.push(...fromBinding(parent.getNameNode(), parent, sf));
|
|
2014
2872
|
return out;
|
|
2015
2873
|
}
|
|
2016
2874
|
function fromBinding(name, decl, sf) {
|
|
2017
2875
|
const out = [];
|
|
2018
|
-
if (
|
|
2876
|
+
if (Node4.isIdentifier(name)) {
|
|
2019
2877
|
for (const ref of references(name.getText(), decl, sf)) {
|
|
2020
2878
|
const p = ref.getParent();
|
|
2021
|
-
if (
|
|
2879
|
+
if (Node4.isPropertyAccessExpression(p) && p.getExpression() === ref && STATUS_PROPS.has(p.getName())) out.push(p);
|
|
2022
2880
|
}
|
|
2023
|
-
} else if (
|
|
2881
|
+
} else if (Node4.isObjectBindingPattern(name)) {
|
|
2024
2882
|
for (const el of name.getElements()) {
|
|
2025
2883
|
const prop = el.getPropertyNameNode()?.getText() ?? el.getName();
|
|
2026
|
-
if (STATUS_PROPS.has(prop) &&
|
|
2884
|
+
if (STATUS_PROPS.has(prop) && Node4.isIdentifier(el.getNameNode())) out.push(...references(el.getName(), decl, sf));
|
|
2027
2885
|
}
|
|
2028
2886
|
}
|
|
2029
2887
|
return out;
|
|
2030
2888
|
}
|
|
2031
2889
|
function references(name, decl, sf) {
|
|
2032
2890
|
const scope = decl.getFirstAncestor(
|
|
2033
|
-
(n) =>
|
|
2891
|
+
(n) => Node4.isBlock(n) || Node4.isArrowFunction(n) || Node4.isFunctionExpression(n) || Node4.isFunctionDeclaration(n) || Node4.isMethodDeclaration(n) || Node4.isCaseClause(n)
|
|
2034
2892
|
) ?? sf;
|
|
2035
|
-
return scope.getDescendantsOfKind(
|
|
2893
|
+
return scope.getDescendantsOfKind(SyntaxKind4.Identifier).filter((id) => {
|
|
2036
2894
|
if (id.getText() !== name || id.getStart() < decl.getEnd()) return false;
|
|
2037
2895
|
const p = id.getParent();
|
|
2038
|
-
if (
|
|
2039
|
-
if (
|
|
2040
|
-
return !(
|
|
2896
|
+
if (Node4.isPropertyAccessExpression(p) && p.getNameNode() === id) return false;
|
|
2897
|
+
if (Node4.isPropertyAssignment(p) && p.getNameNode() === id) return false;
|
|
2898
|
+
return !(Node4.isBindingElement(p) || Node4.isVariableDeclaration(p) || Node4.isParameterDeclaration(p));
|
|
2041
2899
|
});
|
|
2042
2900
|
}
|
|
2043
2901
|
|
|
2044
2902
|
// packages/repair/src/recipe.ts
|
|
2045
|
-
import { Project as Project2, ScriptKind as ScriptKind2, SyntaxKind as
|
|
2903
|
+
import { Project as Project2, ScriptKind as ScriptKind2, SyntaxKind as SyntaxKind5 } from "ts-morph";
|
|
2046
2904
|
function findCall(sf, site) {
|
|
2047
|
-
return sf.getDescendantsOfKind(
|
|
2905
|
+
return sf.getDescendantsOfKind(SyntaxKind5.CallExpression).filter((c) => {
|
|
2048
2906
|
const pos = sf.getLineAndColumnAtPos(c.getStart());
|
|
2049
2907
|
return pos.line === site.line && pos.column === site.column;
|
|
2050
2908
|
}).sort((a, b) => a.getWidth() - b.getWidth())[0];
|
|
@@ -2073,7 +2931,7 @@ function runRecipe(recipes, advisory, callSite, text) {
|
|
|
2073
2931
|
|
|
2074
2932
|
// packages/repair/src/contract.ts
|
|
2075
2933
|
import path7 from "path";
|
|
2076
|
-
import { Node as
|
|
2934
|
+
import { Node as Node5, Project as Project3, ScriptKind as ScriptKind3, SyntaxKind as SyntaxKind6 } from "ts-morph";
|
|
2077
2935
|
|
|
2078
2936
|
// packages/repair/src/synth.ts
|
|
2079
2937
|
function synthesizeBody(shape) {
|
|
@@ -2132,70 +2990,339 @@ function sampleString(classes) {
|
|
|
2132
2990
|
if (classes.includes("token")) return "shapi_value";
|
|
2133
2991
|
return "shapi text";
|
|
2134
2992
|
}
|
|
2993
|
+
function bodyFromPaths(paths, values = {}) {
|
|
2994
|
+
const all = [.../* @__PURE__ */ new Set([...paths, ...Object.keys(values)])];
|
|
2995
|
+
const isLeaf = (p) => !all.some((o) => o !== p && (o.startsWith(`${p}.`) || o.startsWith(`${p}[]`)));
|
|
2996
|
+
const shape = {
|
|
2997
|
+
status: 200,
|
|
2998
|
+
headers: {},
|
|
2999
|
+
headerNames: [],
|
|
3000
|
+
body: {
|
|
3001
|
+
kind: "json",
|
|
3002
|
+
rootType: all.some((p) => p.startsWith("[]")) ? "array" : "object",
|
|
3003
|
+
fields: all.map((path13) => ({ path: path13, pathHash: path13, types: isLeaf(path13) ? ["string"] : [], classes: [], occurrences: 1, nullRate: 0 }))
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
const body3 = synthesizeBody(shape);
|
|
3007
|
+
for (const [path13, value2] of Object.entries(values)) setPath(body3, tokens(path13), value2);
|
|
3008
|
+
return body3;
|
|
3009
|
+
}
|
|
3010
|
+
function setPath(target, path13, value2) {
|
|
3011
|
+
let cur = target;
|
|
3012
|
+
for (let i = 0; i < path13.length; i++) {
|
|
3013
|
+
const t = path13[i];
|
|
3014
|
+
const last = i === path13.length - 1;
|
|
3015
|
+
if (t === "[]") {
|
|
3016
|
+
const arr = cur;
|
|
3017
|
+
if (last) arr[0] = value2;
|
|
3018
|
+
else cur = arr[0];
|
|
3019
|
+
} else {
|
|
3020
|
+
const obj = cur;
|
|
3021
|
+
if (last) obj[t] = value2;
|
|
3022
|
+
else cur = obj[t];
|
|
3023
|
+
}
|
|
3024
|
+
if (cur === void 0 || cur === null) return;
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
2135
3027
|
|
|
2136
3028
|
// packages/repair/src/contract.ts
|
|
2137
3029
|
var CONTRACT_DIR = "test/shapi";
|
|
3030
|
+
var RETIRED_STATUS = 410;
|
|
2138
3031
|
function generateContractTest(input) {
|
|
2139
3032
|
const { callSite, advisory, before, after } = input;
|
|
2140
|
-
|
|
2141
|
-
return { status: "unsupported", reason: `contract tests support global fetch call sites; this one uses ${callSite.client}` };
|
|
2142
|
-
}
|
|
3033
|
+
const fix2 = advisory.fix;
|
|
2143
3034
|
const project = new Project3({ useInMemoryFileSystem: true, skipFileDependencyResolution: true, compilerOptions: { allowJs: true } });
|
|
2144
3035
|
const sf = project.createSourceFile(`/${callSite.file}`, input.sourceText, {
|
|
2145
3036
|
scriptKind: /\.(c|m)?jsx?$/.test(callSite.file) ? ScriptKind3.JS : ScriptKind3.TS
|
|
2146
3037
|
});
|
|
2147
3038
|
const call = findCall(sf, callSite);
|
|
2148
3039
|
if (!call) return { status: "unsupported", reason: `no call at ${callSite.file}:${callSite.line}:${callSite.column}` };
|
|
3040
|
+
const transport = transportFor(callSite, sf);
|
|
3041
|
+
if ("reason" in transport) return { status: "unsupported", reason: transport.reason };
|
|
2149
3042
|
const fn = exportedFunction(call);
|
|
2150
3043
|
if (!fn) return { status: "unsupported", reason: "the call site is not inside an exported function" };
|
|
2151
|
-
|
|
3044
|
+
const file = input.file ?? `${CONTRACT_DIR}/${advisory.id}.test.ts`;
|
|
3045
|
+
const importPath = toImport(path7.posix.relative(path7.posix.dirname(file), callSite.file));
|
|
3046
|
+
const parts = {
|
|
3047
|
+
header: `// Generated by SHAPI for advisory ${advisory.id}: ${advisory.title}
|
|
3048
|
+
// Call site: ${callSite.file}:${callSite.line} (${callSite.method} ${callSite.endpoint}, via ${callSite.client})`,
|
|
3049
|
+
fn: fn.name,
|
|
3050
|
+
importPath,
|
|
3051
|
+
args: fn.params.map((p) => argument(p, sf)).join(", "),
|
|
3052
|
+
advisory,
|
|
3053
|
+
transport
|
|
3054
|
+
};
|
|
3055
|
+
if (fix2 && (fix2.path || fix2.query || fix2.header)) {
|
|
3056
|
+
if (callSite.kind === "sdk") return { status: "unsupported", reason: `the fix changes the request, which ${callSite.client} builds itself` };
|
|
3057
|
+
return { status: "generated", file, content: requestTest({ ...parts, fix: fix2, body: renamePair(callSite, fix2).after }), functionName: fn.name };
|
|
3058
|
+
}
|
|
3059
|
+
if (fix2 && (fix2.fields || fix2.enums)) {
|
|
3060
|
+
return { status: "generated", file, content: sameResultTest({ ...parts, ...renamePair(callSite, fix2) }), functionName: fn.name };
|
|
3061
|
+
}
|
|
3062
|
+
if (!before || !after) return { status: "unsupported", reason: "no before/after responses for this endpoint, and the advisory has no fix to build a test from" };
|
|
2152
3063
|
try {
|
|
2153
|
-
|
|
3064
|
+
return { status: "generated", file, content: recordedTest({ ...parts, before, after }), functionName: fn.name };
|
|
2154
3065
|
} catch (err) {
|
|
2155
3066
|
return { status: "unsupported", reason: err instanceof Error ? err.message : String(err) };
|
|
2156
3067
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
3068
|
+
}
|
|
3069
|
+
function transportFor(site, sf) {
|
|
3070
|
+
if (site.kind === "http") {
|
|
3071
|
+
if (site.client === "fetch" || site.client === "ky") return { mock: "", stubFetch: true, name: "fetch" };
|
|
3072
|
+
if (site.client === "node-fetch") {
|
|
3073
|
+
return { mock: `vi.mock("node-fetch", () => ({ default: (input: RequestInfo | URL, init?: RequestInit) => globalThis.fetch(input, init) }));
|
|
3074
|
+
`, stubFetch: true, name: "node-fetch" };
|
|
3075
|
+
}
|
|
3076
|
+
if (site.client === "axios") return { mock: AXIOS_FAKE, stubFetch: false, name: "axios" };
|
|
3077
|
+
if (site.client === "got") return { mock: GOT_FAKE, stubFetch: false, name: "got" };
|
|
3078
|
+
return { reason: `contract tests support fetch, node-fetch, ky, axios, got and SDK call sites; this one uses ${site.client}` };
|
|
3079
|
+
}
|
|
3080
|
+
const decl = sf.getImportDeclarations().find((d) => d.getModuleSpecifierValue() === site.client);
|
|
3081
|
+
if (!decl) return { reason: `${site.file} does not import ${site.client} with an import statement` };
|
|
3082
|
+
const names = decl.getNamedImports().map((n) => n.getName());
|
|
3083
|
+
const exports = [`default: client`, ...names.map((n) => `${JSON.stringify(n)}: client`)].join(", ");
|
|
3084
|
+
const url = `https://${site.host ?? "sdk.invalid"}${site.endpoint ?? "/"}`;
|
|
3085
|
+
const envelope = site.provider === "github" ? `{ status: res.status, data: res.body, headers: res.headers }` : `res.body`;
|
|
3086
|
+
return {
|
|
3087
|
+
mock: `vi.mock(${JSON.stringify(site.client)}, () => {
|
|
3088
|
+
const call = async () => {
|
|
3089
|
+
const res = shapi.respond({ method: ${JSON.stringify(site.method ?? "GET")}, url: ${JSON.stringify(url)}, headers: {} });
|
|
3090
|
+
if (res.status >= 400) throw Object.assign(new Error(\`${site.client} request failed with \${res.status}\`), { status: res.status, statusCode: res.status, response: { status: res.status, data: res.body } });
|
|
3091
|
+
return ${envelope};
|
|
3092
|
+
};
|
|
3093
|
+
const client: any = new Proxy(function () {}, {
|
|
3094
|
+
get: (_t, prop) => (prop === "then" || typeof prop === "symbol" ? undefined : client),
|
|
3095
|
+
apply: () => call(),
|
|
3096
|
+
construct: () => client,
|
|
3097
|
+
});
|
|
3098
|
+
return { ${exports} };
|
|
3099
|
+
});
|
|
3100
|
+
`,
|
|
3101
|
+
stubFetch: false,
|
|
3102
|
+
name: `a fake ${site.client} client`
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
function preamble(p) {
|
|
3106
|
+
return `import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2166
3107
|
|
|
3108
|
+
type ShapiRequest = { method: string; url: string; headers: Record<string, string> };
|
|
3109
|
+
type ShapiResponse = { status: number; body: unknown; headers: Record<string, string> };
|
|
3110
|
+
const shapi = vi.hoisted(() => ({
|
|
3111
|
+
respond: (_req: { method: string; url: string; headers: Record<string, string> }): { status: number; body: unknown; headers: Record<string, string> } => ({ status: 200, body: null, headers: {} }),
|
|
3112
|
+
lower: (h: unknown): Record<string, string> =>
|
|
3113
|
+
Object.fromEntries(Object.entries((h ?? {}) as Record<string, unknown>).filter(([, v]) => v !== undefined).map(([k, v]) => [k.toLowerCase(), String(v)])),
|
|
3114
|
+
}));
|
|
3115
|
+
${p.transport.mock}
|
|
3116
|
+
import { ${p.fn} } from "${p.importPath}";
|
|
3117
|
+
${p.transport.stubFetch ? `
|
|
3118
|
+
const shapiFetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
3119
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
3120
|
+
const headers = Object.fromEntries(new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)).entries());
|
|
3121
|
+
const res = shapi.respond({ method: (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase(), url, headers });
|
|
3122
|
+
return new Response(res.body === null ? null : JSON.stringify(res.body), { status: res.status, headers: res.headers });
|
|
3123
|
+
});
|
|
3124
|
+
` : ""}
|
|
3125
|
+
afterEach(() => {
|
|
3126
|
+
vi.unstubAllGlobals();
|
|
3127
|
+
});
|
|
3128
|
+
|
|
3129
|
+
async function run(respond: (req: ShapiRequest) => ShapiResponse): Promise<unknown> {
|
|
3130
|
+
shapi.respond = respond;${p.transport.stubFetch ? `
|
|
3131
|
+
vi.stubGlobal("fetch", shapiFetch);` : ""}
|
|
3132
|
+
return ${p.fn}(${p.args});
|
|
3133
|
+
}
|
|
3134
|
+
`;
|
|
3135
|
+
}
|
|
3136
|
+
var JSON_HEADERS = `{ "content-type": "application/json" }`;
|
|
3137
|
+
function requestTest(p) {
|
|
3138
|
+
const checks = [];
|
|
3139
|
+
if (p.fix.path) checks.push(`url.href.includes(${JSON.stringify(p.fix.path.from)})`);
|
|
3140
|
+
if (p.fix.query) {
|
|
3141
|
+
const name = JSON.stringify(p.fix.query.name);
|
|
3142
|
+
const below = p.advisory.trigger.query?.find((q) => q.name === p.fix.query.name)?.below;
|
|
3143
|
+
checks.push(
|
|
3144
|
+
below === void 0 ? `(url.searchParams.has(${name}) && url.searchParams.get(${name}) !== ${JSON.stringify(p.fix.query.value)})` : `(url.searchParams.has(${name}) && Number(url.searchParams.get(${name})) < ${below})`
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
if (p.fix.header) {
|
|
3148
|
+
const name = JSON.stringify(p.fix.header.name.toLowerCase());
|
|
3149
|
+
checks.push(`(${name} in req.headers && req.headers[${name}] !== ${JSON.stringify(p.fix.header.value)})`);
|
|
3150
|
+
}
|
|
3151
|
+
return `${p.header}
|
|
3152
|
+
// The provider no longer accepts the old request: ${p.transport.name} is replaced by a stub that answers it
|
|
3153
|
+
// with ${RETIRED_STATUS}, and answers anything else with a body holding the fields the code reads. The test
|
|
3154
|
+
// fails until ${p.fn}() sends the new request (${describeFix(p.fix)}) and still works.
|
|
3155
|
+
${preamble(p)}
|
|
3156
|
+
const body = ${JSON.stringify(p.body)};
|
|
3157
|
+
|
|
3158
|
+
function retired(req: ShapiRequest): boolean {
|
|
3159
|
+
const url = new URL(req.url, "https://shapi.invalid");
|
|
3160
|
+
return ${checks.join(" || ")};
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
describe("SHAPI contract: ${p.advisory.id}", () => {
|
|
3164
|
+
it("${p.fn} sends the new request and handles the response", async () => {
|
|
3165
|
+
const sent: boolean[] = [];
|
|
3166
|
+
await run((req) => {
|
|
3167
|
+
const old = retired(req);
|
|
3168
|
+
sent.push(old);
|
|
3169
|
+
return old ? { status: ${RETIRED_STATUS}, body: { error: "retired request (SHAPI contract test)" }, headers: ${JSON_HEADERS} } : { status: 200, body, headers: ${JSON_HEADERS} };
|
|
3170
|
+
});
|
|
3171
|
+
expect(sent.length).toBeGreaterThan(0);
|
|
3172
|
+
expect(sent).not.toContain(true);
|
|
3173
|
+
});
|
|
3174
|
+
});
|
|
3175
|
+
`;
|
|
3176
|
+
}
|
|
3177
|
+
function sameResultTest(p) {
|
|
3178
|
+
return `${p.header}
|
|
3179
|
+
// The provider renamed fields or enum values. This test calls ${p.fn}() once with the old response and
|
|
3180
|
+
// once with the new one (${p.transport.name} replaced by a stub), and expects the same result: it fails
|
|
3181
|
+
// while the code only reads the old names.
|
|
3182
|
+
${preamble(p)}
|
|
2167
3183
|
const responses = [
|
|
2168
|
-
${
|
|
3184
|
+
{ name: "old", body: ${JSON.stringify(p.before)} },
|
|
3185
|
+
{ name: "new", body: ${JSON.stringify(p.after)} },
|
|
2169
3186
|
];
|
|
2170
3187
|
|
|
2171
|
-
describe("SHAPI contract: ${advisory.id}", () => {
|
|
2172
|
-
|
|
2173
|
-
|
|
3188
|
+
describe("SHAPI contract: ${p.advisory.id}", () => {
|
|
3189
|
+
it("${p.fn} returns the same result for the old and the new response", async () => {
|
|
3190
|
+
const results: unknown[] = [];
|
|
3191
|
+
for (const r of responses) results.push(await run(() => ({ status: 200, body: r.body, headers: ${JSON_HEADERS} })));
|
|
3192
|
+
expect(results[1]).toEqual(results[0]);
|
|
2174
3193
|
});
|
|
3194
|
+
});
|
|
3195
|
+
`;
|
|
3196
|
+
}
|
|
3197
|
+
function recordedTest(p) {
|
|
3198
|
+
const responses = [response(`before (${p.before.status})`, p.before), response(`after (${p.after.status})`, p.after)].join(",\n");
|
|
3199
|
+
return `${p.header}
|
|
3200
|
+
// Calls ${p.fn}() with ${p.transport.name} returning the response the code was written for and the one
|
|
3201
|
+
// ${p.advisory.provider} sends now. It fails while the code accepts only the old response.
|
|
3202
|
+
${preamble(p)}
|
|
3203
|
+
const responses = [
|
|
3204
|
+
${responses},
|
|
3205
|
+
];
|
|
2175
3206
|
|
|
3207
|
+
describe("SHAPI contract: ${p.advisory.id}", () => {
|
|
2176
3208
|
for (const r of responses) {
|
|
2177
|
-
it(\`${fn
|
|
2178
|
-
const
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
expect(fetchMock).toHaveBeenCalled();
|
|
3209
|
+
it(\`${p.fn} handles the \${r.name} response\`, async () => {
|
|
3210
|
+
const seen: ShapiRequest[] = [];
|
|
3211
|
+
await run((req) => (seen.push(req), { status: r.status, body: r.body, headers: r.headers }));
|
|
3212
|
+
expect(seen.length).toBeGreaterThan(0);
|
|
2182
3213
|
});
|
|
2183
3214
|
}
|
|
2184
3215
|
});
|
|
2185
3216
|
`;
|
|
2186
|
-
return { status: "generated", file, content, functionName: fn.name };
|
|
2187
3217
|
}
|
|
3218
|
+
function renamePair(site, fix2) {
|
|
3219
|
+
const paths = site.surface.fields;
|
|
3220
|
+
const rename = (p) => {
|
|
3221
|
+
for (const { from, to } of fix2.fields ?? []) {
|
|
3222
|
+
if (!/[.[]/.test(from)) {
|
|
3223
|
+
const segs = p.split(".");
|
|
3224
|
+
const i = segs.findIndex((seg) => seg.replace(/\[\]/g, "") === from);
|
|
3225
|
+
if (i >= 0) return [...segs.slice(0, i), segs[i].replace(from, to), ...segs.slice(i + 1)].join(".");
|
|
3226
|
+
} else if (p === from || p.startsWith(`${from}.`) || p.startsWith(`${from}[]`)) return to + p.slice(from.length);
|
|
3227
|
+
}
|
|
3228
|
+
return p;
|
|
3229
|
+
};
|
|
3230
|
+
const oldValues = {};
|
|
3231
|
+
const newValues = {};
|
|
3232
|
+
for (const { field, values } of fix2.enums ?? []) {
|
|
3233
|
+
const compared = site.surface.enums.find((e) => e.field === field)?.values ?? [];
|
|
3234
|
+
const pick = values.find((v) => compared.includes(v.from)) ?? values[0];
|
|
3235
|
+
oldValues[field] = pick.from;
|
|
3236
|
+
newValues[rename(field)] = pick.to;
|
|
3237
|
+
}
|
|
3238
|
+
return { before: bodyFromPaths(paths, oldValues), after: bodyFromPaths(paths.map(rename), newValues) };
|
|
3239
|
+
}
|
|
3240
|
+
function describeFix(fix2) {
|
|
3241
|
+
const parts = [];
|
|
3242
|
+
if (fix2.path) parts.push(`${fix2.path.to} instead of ${fix2.path.from}`);
|
|
3243
|
+
if (fix2.query) parts.push(`${fix2.query.name}=${fix2.query.value}`);
|
|
3244
|
+
if (fix2.header) parts.push(`${fix2.header.name}: ${fix2.header.value}`);
|
|
3245
|
+
return parts.join(", ");
|
|
3246
|
+
}
|
|
3247
|
+
var AXIOS_FAKE = `vi.mock("axios", () => {
|
|
3248
|
+
class AxiosError extends Error {
|
|
3249
|
+
isAxiosError = true;
|
|
3250
|
+
constructor(message: string, readonly response?: unknown, readonly status?: number) {
|
|
3251
|
+
super(message);
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
const build = (defaults: any = {}): any => {
|
|
3255
|
+
const send = async (config: any) => {
|
|
3256
|
+
const c = { ...defaults, ...config, headers: { ...defaults.headers, ...config.headers }, params: { ...defaults.params, ...config.params } };
|
|
3257
|
+
let url = String(c.url ?? "");
|
|
3258
|
+
if (c.baseURL && !/^[a-z]+:\\/\\//i.test(url)) url = String(c.baseURL).replace(/\\/+$/, "") + "/" + url.replace(/^\\/+/, "");
|
|
3259
|
+
const qs = new URLSearchParams(Object.entries(c.params).filter(([, v]) => v !== undefined && v !== null).map(([k, v]) => [k, String(v)])).toString();
|
|
3260
|
+
if (qs) url += (url.includes("?") ? "&" : "?") + qs;
|
|
3261
|
+
const res = shapi.respond({ method: String(c.method ?? "get").toUpperCase(), url, headers: shapi.lower(c.headers) });
|
|
3262
|
+
const response = { status: res.status, statusText: "", headers: res.headers, data: res.body, config: c };
|
|
3263
|
+
if (res.status < 200 || res.status >= 300) throw new AxiosError(\`Request failed with status code \${res.status}\`, response, res.status);
|
|
3264
|
+
return response;
|
|
3265
|
+
};
|
|
3266
|
+
const instance: any = (a: any, b?: any) => send(typeof a === "string" ? { ...b, url: a } : a);
|
|
3267
|
+
instance.request = (c: any) => send(c);
|
|
3268
|
+
for (const m of ["get", "delete", "head", "options"]) instance[m] = (url: string, c: any = {}) => send({ ...c, url, method: m });
|
|
3269
|
+
for (const m of ["post", "put", "patch"]) instance[m] = (url: string, data: unknown, c: any = {}) => send({ ...c, url, data, method: m });
|
|
3270
|
+
instance.create = (d: any = {}) => build({ ...defaults, ...d, headers: { ...defaults.headers, ...d.headers } });
|
|
3271
|
+
instance.defaults = defaults;
|
|
3272
|
+
instance.interceptors = { request: { use: () => 0, eject: () => {} }, response: { use: () => 0, eject: () => {} } };
|
|
3273
|
+
instance.isAxiosError = (e: any) => Boolean(e?.isAxiosError);
|
|
3274
|
+
return instance;
|
|
3275
|
+
};
|
|
3276
|
+
const axios = build();
|
|
3277
|
+
return { default: axios, create: axios.create, isAxiosError: axios.isAxiosError, AxiosError };
|
|
3278
|
+
});
|
|
3279
|
+
`;
|
|
3280
|
+
var GOT_FAKE = `vi.mock("got", () => {
|
|
3281
|
+
class HTTPError extends Error {
|
|
3282
|
+
constructor(readonly response: any) {
|
|
3283
|
+
super(\`Response code \${response.statusCode}\`);
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
const build = (defaults: any = {}): any => {
|
|
3287
|
+
const call = (input: any, options: any = {}) => {
|
|
3288
|
+
const fromObject = typeof input === "object" && !(input instanceof URL);
|
|
3289
|
+
const o = { ...defaults, ...(fromObject ? input : options), headers: { ...defaults.headers, ...(fromObject ? input.headers : options.headers) } };
|
|
3290
|
+
let url = fromObject ? String(o.url ?? "") : String(input);
|
|
3291
|
+
if (o.prefixUrl) url = String(o.prefixUrl).replace(/\\/+$/, "") + "/" + url.replace(/^\\/+/, "");
|
|
3292
|
+
const sp = o.searchParams;
|
|
3293
|
+
const qs = sp ? new URLSearchParams(typeof sp === "string" || sp instanceof URLSearchParams ? sp : Object.entries(sp).map(([k, v]) => [k, String(v)])).toString() : "";
|
|
3294
|
+
if (qs) url += (url.includes("?") ? "&" : "?") + qs;
|
|
3295
|
+
const res = shapi.respond({ method: String(o.method ?? "GET").toUpperCase(), url, headers: shapi.lower(o.headers) });
|
|
3296
|
+
const text = res.body === null ? "" : JSON.stringify(res.body);
|
|
3297
|
+
const response = { statusCode: res.status, headers: res.headers, body: o.responseType === "json" ? res.body : text, url };
|
|
3298
|
+
const failed = (o.throwHttpErrors ?? true) && res.status >= 400;
|
|
3299
|
+
const settle = <T,>(value: T): Promise<T> => (failed ? Promise.reject(new HTTPError(response)) : Promise.resolve(value));
|
|
3300
|
+
const promise: any = settle(response);
|
|
3301
|
+
promise.catch(() => {});
|
|
3302
|
+
promise.json = () => settle(res.body);
|
|
3303
|
+
promise.text = () => settle(text);
|
|
3304
|
+
promise.buffer = () => settle(Buffer.from(text));
|
|
3305
|
+
return promise;
|
|
3306
|
+
};
|
|
3307
|
+
const got: any = (input: any, options?: any) => call(input, options);
|
|
3308
|
+
for (const m of ["get", "post", "put", "patch", "delete", "head"]) got[m] = (input: any, options: any = {}) => call(input, { ...options, method: m.toUpperCase() });
|
|
3309
|
+
got.extend = (...opts: any[]) => build(Object.assign({}, defaults, ...opts));
|
|
3310
|
+
return got;
|
|
3311
|
+
};
|
|
3312
|
+
return { default: build(), HTTPError };
|
|
3313
|
+
});
|
|
3314
|
+
`;
|
|
2188
3315
|
function response(name, shape) {
|
|
2189
|
-
const
|
|
3316
|
+
const body3 = synthesizeBody(shape);
|
|
2190
3317
|
const headers = shape.headers["content-type"] ? { "content-type": shape.headers["content-type"] } : {};
|
|
2191
|
-
return ` { name: ${JSON.stringify(name)}, status: ${shape.status}, headers: ${JSON.stringify(headers)}, body: ${JSON.stringify(
|
|
3318
|
+
return ` { name: ${JSON.stringify(name)}, status: ${shape.status}, headers: ${JSON.stringify(headers)}, body: ${JSON.stringify(body3)} }`;
|
|
2192
3319
|
}
|
|
2193
3320
|
function exportedFunction(call) {
|
|
2194
3321
|
for (const anc of call.getAncestors()) {
|
|
2195
|
-
if (
|
|
2196
|
-
if (
|
|
3322
|
+
if (Node5.isFunctionDeclaration(anc) && anc.isExported() && anc.getName()) return { name: anc.getName(), params: anc.getParameters() };
|
|
3323
|
+
if (Node5.isArrowFunction(anc) || Node5.isFunctionExpression(anc)) {
|
|
2197
3324
|
const decl = anc.getParent();
|
|
2198
|
-
if (
|
|
3325
|
+
if (Node5.isVariableDeclaration(decl) && decl.getVariableStatement()?.isExported()) return { name: decl.getName(), params: anc.getParameters() };
|
|
2199
3326
|
}
|
|
2200
3327
|
}
|
|
2201
3328
|
return void 0;
|
|
@@ -2207,22 +3334,22 @@ function argument(p, sf) {
|
|
|
2207
3334
|
function valueFor(type, name, sf, depth) {
|
|
2208
3335
|
if (depth > 4) return "undefined as never";
|
|
2209
3336
|
switch (type.getKind()) {
|
|
2210
|
-
case
|
|
3337
|
+
case SyntaxKind6.StringKeyword:
|
|
2211
3338
|
return JSON.stringify(`shapi-${name}`);
|
|
2212
|
-
case
|
|
3339
|
+
case SyntaxKind6.NumberKeyword:
|
|
2213
3340
|
return "1";
|
|
2214
|
-
case
|
|
3341
|
+
case SyntaxKind6.BooleanKeyword:
|
|
2215
3342
|
return "true";
|
|
2216
3343
|
}
|
|
2217
|
-
if (
|
|
2218
|
-
if (
|
|
3344
|
+
if (Node5.isTypeLiteral(type)) return objectFrom(type.getProperties(), sf, depth);
|
|
3345
|
+
if (Node5.isTypeReference(type)) {
|
|
2219
3346
|
const ref = type.getTypeName().getText();
|
|
2220
3347
|
const iface = sf.getInterface(ref);
|
|
2221
3348
|
if (iface) return objectFrom(iface.getProperties(), sf, depth);
|
|
2222
3349
|
const alias = sf.getTypeAlias(ref)?.getTypeNode();
|
|
2223
3350
|
if (alias) return valueFor(alias, name, sf, depth + 1);
|
|
2224
3351
|
}
|
|
2225
|
-
if (
|
|
3352
|
+
if (Node5.isArrayTypeNode(type)) return "[]";
|
|
2226
3353
|
return "undefined as never";
|
|
2227
3354
|
}
|
|
2228
3355
|
function objectFrom(props, sf, depth) {
|
|
@@ -2242,13 +3369,13 @@ import path10 from "path";
|
|
|
2242
3369
|
|
|
2243
3370
|
// packages/repair/src/runner.ts
|
|
2244
3371
|
import { spawnSync } from "child_process";
|
|
2245
|
-
import { existsSync } from "fs";
|
|
2246
|
-
import { createRequire } from "module";
|
|
3372
|
+
import { existsSync as existsSync2 } from "fs";
|
|
3373
|
+
import { createRequire as createRequire2 } from "module";
|
|
2247
3374
|
import path8 from "path";
|
|
2248
3375
|
function vitestCli(dir) {
|
|
2249
3376
|
const local = path8.join(dir, "node_modules", "vitest", "vitest.mjs");
|
|
2250
|
-
if (
|
|
2251
|
-
const require2 =
|
|
3377
|
+
if (existsSync2(local)) return local;
|
|
3378
|
+
const require2 = createRequire2(import.meta.url);
|
|
2252
3379
|
return path8.join(path8.dirname(require2.resolve("vitest/package.json")), "vitest.mjs");
|
|
2253
3380
|
}
|
|
2254
3381
|
function runTestFile(dir, file, timeoutMs = 12e4) {
|
|
@@ -2272,7 +3399,7 @@ function runFailure(output) {
|
|
|
2272
3399
|
}
|
|
2273
3400
|
|
|
2274
3401
|
// packages/repair/src/workspace.ts
|
|
2275
|
-
import { cpSync, existsSync as
|
|
3402
|
+
import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync4, mkdtempSync, readFileSync as readFileSync5, symlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
2276
3403
|
import os from "os";
|
|
2277
3404
|
import path9 from "path";
|
|
2278
3405
|
var COPY_SKIP = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "coverage", ".shapi"]);
|
|
@@ -2283,12 +3410,12 @@ function localWorkspace(repoDir) {
|
|
|
2283
3410
|
filter: (src) => !COPY_SKIP.has(path9.basename(src)) || path9.resolve(src) === path9.resolve(repoDir)
|
|
2284
3411
|
});
|
|
2285
3412
|
const modules = path9.join(repoDir, "node_modules");
|
|
2286
|
-
if (
|
|
3413
|
+
if (existsSync3(modules)) symlinkSync(modules, path9.join(dir, "node_modules"), "junction");
|
|
2287
3414
|
return {
|
|
2288
3415
|
location: dir,
|
|
2289
3416
|
async read(file) {
|
|
2290
3417
|
const abs = path9.join(dir, file);
|
|
2291
|
-
return
|
|
3418
|
+
return existsSync3(abs) ? readFileSync5(abs, "utf8") : void 0;
|
|
2292
3419
|
},
|
|
2293
3420
|
async write(file, content) {
|
|
2294
3421
|
const abs = path9.join(dir, file);
|
|
@@ -2306,10 +3433,13 @@ var PROTECTED = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "covera
|
|
|
2306
3433
|
async function repairCallSite(input) {
|
|
2307
3434
|
const { advisory, callSite } = input;
|
|
2308
3435
|
if (!input.workspace && !input.repoDir) throw new Error("repairCallSite needs a workspace or a repoDir");
|
|
3436
|
+
if (!/\.(c|m)?[jt]sx?$/.test(callSite.file)) {
|
|
3437
|
+
return { status: "unsupported", changes: [], attempts: [], reason: `fixes support TypeScript and JavaScript; ${callSite.file} is scanned only` };
|
|
3438
|
+
}
|
|
2309
3439
|
const ws = input.workspace ?? localWorkspace(input.repoDir);
|
|
2310
3440
|
const original = await ws.read(callSite.file);
|
|
2311
3441
|
if (original === void 0) return { status: "unsupported", sandbox: ws.location, changes: [], attempts: [], reason: `${callSite.file} is not in the repository` };
|
|
2312
|
-
const test = generateContractTest({ callSite, advisory, before: input.before, after: input.after, sourceText: original });
|
|
3442
|
+
const test = generateContractTest({ callSite, advisory, before: input.before, after: input.after, sourceText: original, file: input.testFile });
|
|
2313
3443
|
if (test.status !== "generated") return { status: "unsupported", changes: [], attempts: [], reason: test.reason };
|
|
2314
3444
|
await ws.write(test.file, test.content);
|
|
2315
3445
|
const testChange = { file: test.file, before: "", after: test.content, notes: ["generated contract test"] };
|
|
@@ -2398,6 +3528,42 @@ function rejected(ws, test, baseline, attempts) {
|
|
|
2398
3528
|
async function restore(ws, changes) {
|
|
2399
3529
|
for (const c of changes) await ws.write(c.file, c.before);
|
|
2400
3530
|
}
|
|
3531
|
+
async function repairAdvisory(input) {
|
|
3532
|
+
if (!input.workspace && !input.repoDir) throw new Error("repairAdvisory needs a workspace or a repoDir");
|
|
3533
|
+
const ws = input.workspace ?? localWorkspace(input.repoDir);
|
|
3534
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3535
|
+
const order = input.callSites.filter((s) => {
|
|
3536
|
+
const key = `${s.file}:${s.line}:${s.column}`;
|
|
3537
|
+
return !seen.has(key) && Boolean(seen.add(key));
|
|
3538
|
+
}).sort((a, b) => a.file.localeCompare(b.file) || b.line - a.line || b.column - a.column);
|
|
3539
|
+
const originals = /* @__PURE__ */ new Map();
|
|
3540
|
+
const sites = [];
|
|
3541
|
+
for (const [i, callSite] of order.entries()) {
|
|
3542
|
+
if (!originals.has(callSite.file)) originals.set(callSite.file, await ws.read(callSite.file) ?? "");
|
|
3543
|
+
const testFile = `test/shapi/${input.advisory.id}${i === 0 ? "" : `.${i + 1}`}.test.ts`;
|
|
3544
|
+
const result = await repairCallSite({ ...input, workspace: ws, callSite, testFile });
|
|
3545
|
+
for (const c of result.changes) if (c.before && !originals.has(c.file)) originals.set(c.file, c.before);
|
|
3546
|
+
sites.push({ callSite, result });
|
|
3547
|
+
}
|
|
3548
|
+
const fixed = sites.filter((s) => s.result.status === "fixed");
|
|
3549
|
+
const needed = sites.filter((s) => s.result.status !== "already-handled");
|
|
3550
|
+
const tests = new Set(fixed.map((s) => s.result.test?.file));
|
|
3551
|
+
const sourceFiles = [...new Set(fixed.flatMap((s) => s.result.changes.filter((c) => !tests.has(c.file)).map((c) => c.file)))].sort();
|
|
3552
|
+
const changes = [];
|
|
3553
|
+
for (const file of sourceFiles) {
|
|
3554
|
+
const notes = fixed.flatMap((s) => s.result.changes.filter((c) => c.file === file).flatMap((c) => c.notes));
|
|
3555
|
+
changes.push({ file, before: originals.get(file) ?? "", after: await ws.read(file) ?? "", notes });
|
|
3556
|
+
}
|
|
3557
|
+
for (const s of fixed) if (s.result.test) changes.push({ file: s.result.test.file, before: "", after: s.result.test.content, notes: ["generated contract test"] });
|
|
3558
|
+
const methods = new Set(fixed.map((s) => s.result.method));
|
|
3559
|
+
return {
|
|
3560
|
+
status: fixed.length === 0 ? "none" : fixed.length === needed.length ? "fixed" : "partial",
|
|
3561
|
+
sites,
|
|
3562
|
+
changes,
|
|
3563
|
+
...fixed.length ? { method: methods.has("llm") ? "llm" : "recipe" } : {},
|
|
3564
|
+
sandbox: ws.location
|
|
3565
|
+
};
|
|
3566
|
+
}
|
|
2401
3567
|
|
|
2402
3568
|
// packages/repair/src/llm.ts
|
|
2403
3569
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -2510,7 +3676,8 @@ ${req.failingOutput.slice(-6e3)}
|
|
|
2510
3676
|
|
|
2511
3677
|
// packages/repair/src/index.ts
|
|
2512
3678
|
var RECIPES = {
|
|
2513
|
-
[statusCodeAcceptSet.id]: statusCodeAcceptSet
|
|
3679
|
+
[statusCodeAcceptSet.id]: statusCodeAcceptSet,
|
|
3680
|
+
[applyFix.id]: applyFix
|
|
2514
3681
|
};
|
|
2515
3682
|
|
|
2516
3683
|
// packages/github-app/src/pr.ts
|
|
@@ -2651,29 +3818,31 @@ function prTitle(advisory) {
|
|
|
2651
3818
|
function prBranch(advisory) {
|
|
2652
3819
|
return `shapi/${advisory.id}`;
|
|
2653
3820
|
}
|
|
2654
|
-
function renderPrBody({ advisory,
|
|
2655
|
-
const
|
|
2656
|
-
const
|
|
2657
|
-
const
|
|
3821
|
+
function renderPrBody({ advisory, repair, shapeSource }) {
|
|
3822
|
+
const fixed = repair.sites.filter((s) => s.result.status === "fixed");
|
|
3823
|
+
const tests = new Set(fixed.map((s) => s.result.test?.file));
|
|
3824
|
+
const notes = repair.changes.filter((c) => !tests.has(c.file)).flatMap((c) => (c.notes.length ? c.notes : [`\`${c.file}\``]).map((n) => `- ${n}`));
|
|
3825
|
+
const method = repair.method === "llm" ? "Claude, verified by the contract tests" : `deterministic recipe \`${advisory.recipeId}\``;
|
|
2658
3826
|
const evidence = advisory.evidence;
|
|
3827
|
+
const where = (s) => `\`${s.callSite.file}:${s.callSite.line}\` (${s.callSite.method} ${s.callSite.endpoint})`;
|
|
3828
|
+
const siteLine = (s) => s.result.status === "fixed" ? `- [x] ${where(s)}: fixed, test \`${s.result.test?.file}\`` : s.result.status === "already-handled" ? `- [x] ${where(s)}: already handles the change` : `- [ ] ${where(s)}: not fixed here (${s.result.reason ?? s.result.status}); needs a manual change`;
|
|
2659
3829
|
const lines = [
|
|
2660
3830
|
`**${advisory.provider}** changed \`${advisory.endpoint.method} ${advisory.endpoint.path}\`: ${advisory.title}.`,
|
|
2661
3831
|
"",
|
|
2662
3832
|
advisory.summary,
|
|
2663
3833
|
"",
|
|
2664
|
-
|
|
2665
|
-
|
|
3834
|
+
`### Affected call sites (${fixed.length} of ${repair.sites.length} fixed here)`,
|
|
3835
|
+
...repair.sites.map(siteLine),
|
|
2666
3836
|
"",
|
|
2667
3837
|
`### Change (${method})`,
|
|
2668
|
-
...notes
|
|
3838
|
+
...notes,
|
|
2669
3839
|
"",
|
|
2670
|
-
|
|
2671
|
-
|
|
3840
|
+
`### Contract test${fixed.length === 1 ? "" : "s"}`,
|
|
3841
|
+
"Each test calls the code with the response it was written for and the one the provider sends now. Each failed before this change and passes after it.",
|
|
2672
3842
|
"",
|
|
2673
|
-
"|
|
|
2674
|
-
"
|
|
2675
|
-
|
|
2676
|
-
`| After this change | ${summary(result.attempts.find((a) => a.outcome === "passed")?.run)} |`,
|
|
3843
|
+
"| Test | Before this change | After this change |",
|
|
3844
|
+
"|---|---|---|",
|
|
3845
|
+
...fixed.map((s) => `| \`${s.result.test?.file}\` | ${summary(s.result.baseline)} | ${summary(s.result.attempts.find((a) => a.outcome === "passed")?.run)} |`),
|
|
2677
3846
|
"",
|
|
2678
3847
|
"### Evidence",
|
|
2679
3848
|
"| | |",
|
|
@@ -2685,13 +3854,13 @@ function renderPrBody({ advisory, callSite, result, affectedInRepo, shapeSource
|
|
|
2685
3854
|
`| Provider notice | ${advisory.publishedNotice ? `[${advisory.publishedNotice.date ?? "link"}](${advisory.publishedNotice.url})` : "none (undocumented change)"} |`,
|
|
2686
3855
|
`| Shapes from | ${shapeSource} |`,
|
|
2687
3856
|
...evidence ? [`| Observed | ${evidence.before.status} at ${evidence.before.observedAt} \u2192 ${evidence.after.status} at ${evidence.after.observedAt} (${evidence.source}) |`] : [],
|
|
2688
|
-
`| Blast radius | ${
|
|
3857
|
+
`| Blast radius | ${repair.sites.length} affected call site${repair.sites.length === 1 ? "" : "s"} in this repo |`,
|
|
2689
3858
|
`| Sources | ${advisory.sources.map((s, i) => `[${i + 1}](${s})`).join(" ")} |`,
|
|
2690
3859
|
"",
|
|
2691
3860
|
`Migration note: ${advisory.migrationNote}`,
|
|
2692
3861
|
"",
|
|
2693
3862
|
"---",
|
|
2694
|
-
"Opened by [SHAPI](https://github.com/SriVarshini09/YC).
|
|
3863
|
+
"Opened by [SHAPI](https://github.com/SriVarshini09/YC). A change is kept only when its contract test failed before it and passes after it."
|
|
2695
3864
|
];
|
|
2696
3865
|
return lines.join("\n") + "\n";
|
|
2697
3866
|
}
|
|
@@ -2712,7 +3881,7 @@ import { Octokit as Octokit2 } from "@octokit/rest";
|
|
|
2712
3881
|
var ResilientOctokit = Octokit2.plugin(throttling, retry);
|
|
2713
3882
|
|
|
2714
3883
|
// packages/github-app/src/snapshot.ts
|
|
2715
|
-
import { Parser } from "tar";
|
|
3884
|
+
import { Parser as Parser2 } from "tar";
|
|
2716
3885
|
|
|
2717
3886
|
// packages/cli/src/scan.ts
|
|
2718
3887
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -2721,16 +3890,19 @@ import os2 from "os";
|
|
|
2721
3890
|
import path11 from "path";
|
|
2722
3891
|
|
|
2723
3892
|
// packages/cli/src/paths.ts
|
|
2724
|
-
import { existsSync as
|
|
2725
|
-
import { fileURLToPath } from "url";
|
|
3893
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3894
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2726
3895
|
function bundledDir(published, monorepo) {
|
|
2727
|
-
const candidates = [new URL(`../${published}`, import.meta.url), new URL(`../../../${monorepo}`, import.meta.url)].map((u) =>
|
|
2728
|
-
return candidates.find((p) =>
|
|
3896
|
+
const candidates = [new URL(`../${published}`, import.meta.url), new URL(`../../../${monorepo}`, import.meta.url)].map((u) => fileURLToPath2(u));
|
|
3897
|
+
return candidates.find((p) => existsSync4(p)) ?? candidates[candidates.length - 1];
|
|
2729
3898
|
}
|
|
2730
3899
|
|
|
2731
3900
|
// packages/cli/src/scan.ts
|
|
2732
|
-
var VERSION = "0.
|
|
3901
|
+
var VERSION = "0.2.0";
|
|
2733
3902
|
var DEFAULT_ADVISORIES = bundledDir("advisories", "advisories");
|
|
3903
|
+
async function preparePython() {
|
|
3904
|
+
await loadPythonParser().catch(() => void 0);
|
|
3905
|
+
}
|
|
2734
3906
|
function scan(target, opts) {
|
|
2735
3907
|
const started = performance.now();
|
|
2736
3908
|
const failOn = parseFailOn(opts.failOn ?? "critical");
|
|
@@ -2756,6 +3928,7 @@ function scan(target, opts) {
|
|
|
2756
3928
|
return {
|
|
2757
3929
|
label: label2,
|
|
2758
3930
|
filesScanned: extracted.filesScanned,
|
|
3931
|
+
skipped: extracted.skipped,
|
|
2759
3932
|
lock,
|
|
2760
3933
|
match,
|
|
2761
3934
|
elapsedMs: Math.round(performance.now() - started),
|
|
@@ -2822,24 +3995,31 @@ async function fix(target, opts, deps) {
|
|
|
2822
3995
|
if (affected.length === 0) deps.log("No affected call sites: nothing to fix.");
|
|
2823
3996
|
const llm = opts.llm ? claudeFixer() : void 0;
|
|
2824
3997
|
const outcomes = [];
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
const
|
|
3998
|
+
const byAdvisory = /* @__PURE__ */ new Map();
|
|
3999
|
+
for (const f of affected) byAdvisory.set(f.advisory.id, [...byAdvisory.get(f.advisory.id) ?? [], f]);
|
|
4000
|
+
for (const findings of byAdvisory.values()) {
|
|
4001
|
+
const advisory = findings[0].advisory;
|
|
4002
|
+
const shapes = advisory.fix ? { source: FIX_SOURCE } : findShapes(advisory, opts);
|
|
2829
4003
|
if (!shapes) {
|
|
2830
4004
|
const skipped = "no before/after shapes for this endpoint (record them with the canary, or add a recording)";
|
|
2831
|
-
|
|
2832
|
-
|
|
4005
|
+
for (const finding of findings) {
|
|
4006
|
+
deps.log(`SKIP ${finding.callSite.file}:${finding.callSite.line} ${advisory.id}: ${skipped}`);
|
|
4007
|
+
outcomes.push({ finding, skipped });
|
|
4008
|
+
}
|
|
2833
4009
|
continue;
|
|
2834
4010
|
}
|
|
2835
|
-
const
|
|
2836
|
-
const
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
4011
|
+
const repair = await repairAdvisory({ workspace: localWorkspace(dir), advisory, callSites: findings.map((f) => f.callSite), before: shapes.before, after: shapes.after, recipes: RECIPES, llm });
|
|
4012
|
+
const mine = [];
|
|
4013
|
+
for (const { callSite, result } of repair.sites) {
|
|
4014
|
+
const finding = findings.find((f) => f.callSite.file === callSite.file && f.callSite.line === callSite.line && f.callSite.column === callSite.column);
|
|
4015
|
+
mine.push({ finding, shapeSource: shapes.source, result });
|
|
4016
|
+
deps.log(`${result.status.toUpperCase().padEnd(15)} ${callSite.file}:${callSite.line} ${advisory.id}${result.method ? ` via ${result.method}` : ""}${result.reason ? `: ${result.reason}` : ""}`);
|
|
4017
|
+
for (const a of result.attempts) deps.log(` ${a.method}: ${a.outcome} (${a.detail})`);
|
|
4018
|
+
}
|
|
4019
|
+
outcomes.push(...mine);
|
|
4020
|
+
if (!repair.changes.length) continue;
|
|
4021
|
+
for (const c of repair.changes) deps.log(` ${c.before ? "M" : "A"} ${c.file}`);
|
|
4022
|
+
const files = repair.changes.map((c) => ({ path: c.file, content: c.after }));
|
|
2843
4023
|
if (opts.write) {
|
|
2844
4024
|
for (const f of files) {
|
|
2845
4025
|
mkdirSync5(path12.dirname(path12.join(dir, f.path)), { recursive: true });
|
|
@@ -2848,22 +4028,18 @@ async function fix(target, opts, deps) {
|
|
|
2848
4028
|
deps.log(` wrote ${files.length} files into ${dir}`);
|
|
2849
4029
|
}
|
|
2850
4030
|
if (opts.pr && remote) {
|
|
4031
|
+
const fixed = repair.sites.filter((s) => s.result.status === "fixed");
|
|
2851
4032
|
const meta = {
|
|
2852
4033
|
title: prTitle(advisory),
|
|
2853
4034
|
branch: prBranch(advisory),
|
|
2854
4035
|
commitMessage: `fix: ${advisory.title}
|
|
2855
4036
|
|
|
2856
|
-
Advisory ${advisory.id}. Contract
|
|
2857
|
-
body: renderPrBody({
|
|
2858
|
-
advisory,
|
|
2859
|
-
callSite,
|
|
2860
|
-
result,
|
|
2861
|
-
affectedInRepo: affected.filter((f) => f.advisory.id === advisory.id).length,
|
|
2862
|
-
shapeSource: shapes.source
|
|
2863
|
-
})
|
|
4037
|
+
Advisory ${advisory.id}, ${fixed.length} of ${repair.sites.length} call sites. Contract tests ${fixed.map((s) => s.result.test?.file).join(", ")} fail before this change and pass after.`,
|
|
4038
|
+
body: renderPrBody({ advisory, repair, shapeSource: shapes.source })
|
|
2864
4039
|
};
|
|
2865
|
-
|
|
2866
|
-
|
|
4040
|
+
const pr = await (deps.deliver ?? deliverToGitHub)(remote, files, meta);
|
|
4041
|
+
for (const o of mine) if (o.result?.status === "fixed") o.pr = pr;
|
|
4042
|
+
deps.log(` pull request: ${pr.url}`);
|
|
2867
4043
|
}
|
|
2868
4044
|
}
|
|
2869
4045
|
return outcomes;
|
|
@@ -2878,8 +4054,9 @@ async function deliverToGitHub(repo, files, meta) {
|
|
|
2878
4054
|
if (outcome.action === "skipped") throw new Error(`not delivered: ${outcome.reason}${outcome.url ? ` (${outcome.url})` : ""}`);
|
|
2879
4055
|
return { url: outcome.url, number: outcome.number, branch: outcome.branch, commit: outcome.commit };
|
|
2880
4056
|
}
|
|
4057
|
+
var FIX_SOURCE = "built from the advisory's fix and the fields the code reads";
|
|
2881
4058
|
function findShapes(advisory, opts) {
|
|
2882
|
-
if (opts.db &&
|
|
4059
|
+
if (opts.db && existsSync5(opts.db)) {
|
|
2883
4060
|
const store = ShapeStore.open(opts.db);
|
|
2884
4061
|
try {
|
|
2885
4062
|
for (const e of store.endpoints()) {
|
|
@@ -2894,7 +4071,7 @@ function findShapes(advisory, opts) {
|
|
|
2894
4071
|
}
|
|
2895
4072
|
}
|
|
2896
4073
|
const dir = opts.recordings ?? DEFAULT_RECORDINGS;
|
|
2897
|
-
if (!
|
|
4074
|
+
if (!existsSync5(dir)) return void 0;
|
|
2898
4075
|
for (const name of readdirSync3(dir).filter((f) => f.endsWith(".json")).sort()) {
|
|
2899
4076
|
const recording = loadRecording(path12.join(dir, name));
|
|
2900
4077
|
if (recording.provider !== advisory.provider || !pathMatches(recording.endpoint, advisory.endpoint)) continue;
|
|
@@ -2929,6 +4106,7 @@ function renderText(o, color) {
|
|
|
2929
4106
|
[plural(o.filesScanned, "file"), plural(lock.callSites.length, "call site"), plural(providers.length, "provider"), `${o.elapsedMs} ms`].join(" \xB7 ")
|
|
2930
4107
|
)
|
|
2931
4108
|
);
|
|
4109
|
+
if (o.skipped.length) out.push(paint("dim", `${plural(o.skipped.length, "file")} not scanned: ${[...new Set(o.skipped.map((s) => s.reason))].join("; ")}`));
|
|
2932
4110
|
out.push("");
|
|
2933
4111
|
out.push(paint("bold", "Providers"));
|
|
2934
4112
|
if (providers.length === 0) out.push(" none found");
|
|
@@ -2988,6 +4166,7 @@ var processIo = {
|
|
|
2988
4166
|
};
|
|
2989
4167
|
async function run(argv, io = processIo, fixDeps = {}) {
|
|
2990
4168
|
let exitCode = 0;
|
|
4169
|
+
await preparePython();
|
|
2991
4170
|
const program = new Command2().name("shapi").description("Map the third-party APIs a codebase depends on and flag the breaking changes that hit it.").version(VERSION).exitOverride().configureOutput({ writeOut: io.stdout, writeErr: io.stderr });
|
|
2992
4171
|
program.command("scan").description("Scan a repository and report affected call sites").argument("[path]", "directory to scan", ".").option("--json", "print a JSON report (includes the api.lock)").option("--out <file>", "write the api.lock to this file").option("--repo <url>", "clone a git repository (shallow) into a temp dir and scan it").option("--advisories <dir>", "advisory directory (default: the bundled advisories/)").option("--fail-on <severity>", "exit 1 when an affected call site is at least this severe (critical|high|medium|low|none)", "critical").option("--no-color", "disable color").addOption(new Option("--now <date>", "evaluate deadlines as of this date").hideHelp()).action((target, opts) => {
|
|
2993
4172
|
const outcome = scan(opts.repo ? void 0 : target, opts);
|
|
@@ -3019,6 +4198,7 @@ export {
|
|
|
3019
4198
|
VERSION,
|
|
3020
4199
|
fix,
|
|
3021
4200
|
main,
|
|
4201
|
+
preparePython,
|
|
3022
4202
|
renderText,
|
|
3023
4203
|
run,
|
|
3024
4204
|
scan,
|