shapi-cli 0.1.0 → 0.3.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 +7 -4
- 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 +1527 -161
- package/dist/tree-sitter-python.wasm +0 -0
- package/package.json +7 -9
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;
|
|
@@ -378,9 +469,25 @@ function providerForHost(host) {
|
|
|
378
469
|
}
|
|
379
470
|
return best?.provider;
|
|
380
471
|
}
|
|
472
|
+
var PATH_SIGNATURES = [
|
|
473
|
+
{ provider: "shopify", path: /^\/admin\/api\/\d{4}-\d{2}(\/|$)/ },
|
|
474
|
+
{ provider: "hubspot", path: /^\/crm-pipelines\/v\d+(\/|$)/ }
|
|
475
|
+
];
|
|
476
|
+
function providerForPath(path13) {
|
|
477
|
+
const hit = PATH_SIGNATURES.find((s) => s.path.test(path13));
|
|
478
|
+
return hit ? getProvider(hit.provider) : void 0;
|
|
479
|
+
}
|
|
381
480
|
function providerForPackage(name) {
|
|
382
481
|
return loadProviders().find((p) => p.sdkPackages.includes(name));
|
|
383
482
|
}
|
|
483
|
+
function providerForPythonModule(module) {
|
|
484
|
+
const top = module.split(".")[0];
|
|
485
|
+
return loadProviders().find((p) => p.python?.packages.some((pkg) => pkg.module === top));
|
|
486
|
+
}
|
|
487
|
+
function providerForPythonPackage(name) {
|
|
488
|
+
const norm = (s) => s.toLowerCase().replace(/[-_.]+/g, "-");
|
|
489
|
+
return loadProviders().find((p) => p.python?.packages.some((pkg) => norm(pkg.pypi) === norm(name)));
|
|
490
|
+
}
|
|
384
491
|
function sdkEndpoint(provider, chain) {
|
|
385
492
|
const methods = provider.sdkMethods ?? {};
|
|
386
493
|
const key = chain.replace(/^rest\./, "");
|
|
@@ -516,11 +623,26 @@ var TriggerSchema = z3.object({
|
|
|
516
623
|
enums: z3.array(z3.object({ field: z3.string() })).optional(),
|
|
517
624
|
/** Affected when the request sets this header (optionally to one of these values). */
|
|
518
625
|
headers: z3.array(z3.object({ name: z3.string(), values: z3.array(z3.string()).optional() })).optional(),
|
|
519
|
-
/**
|
|
520
|
-
|
|
626
|
+
/**
|
|
627
|
+
* Affected when the request sets this query parameter: optionally to a number below `below`, or to
|
|
628
|
+
* one of `values` (a comma-separated value such as `metric=a,b` matches when any item does).
|
|
629
|
+
*/
|
|
630
|
+
query: z3.array(z3.object({ name: z3.string(), below: z3.number().optional(), values: z3.array(z3.string()).optional() })).optional(),
|
|
521
631
|
/** Every call to the endpoint is affected (sunsets, auth and scope changes). */
|
|
522
632
|
any: z3.boolean().optional()
|
|
523
633
|
}).refine((t) => Object.values(t).some((v) => v !== void 0 && v !== false), "trigger needs at least one condition");
|
|
634
|
+
var FixSchema = z3.object({
|
|
635
|
+
/** Replace this part of the request path, for example "/v21.0/" with "/v26.0/". */
|
|
636
|
+
path: z3.object({ from: z3.string().min(1), to: z3.string().min(1) }).optional(),
|
|
637
|
+
/** Send this query parameter with this value, for example minorversion=75. */
|
|
638
|
+
query: z3.object({ name: z3.string().min(1), value: z3.string() }).optional(),
|
|
639
|
+
/** Send this request header with this value, for example Stripe-Version. */
|
|
640
|
+
header: z3.object({ name: z3.string().min(1), value: z3.string() }).optional(),
|
|
641
|
+
/** Response fields that were renamed: the code reads the new name, falling back to the old one. */
|
|
642
|
+
fields: z3.array(z3.object({ from: z3.string().min(1), to: z3.string().min(1) })).optional(),
|
|
643
|
+
/** Enum values that were renamed: comparisons with an old value also accept the new one. */
|
|
644
|
+
enums: z3.array(z3.object({ field: z3.string().min(1), values: z3.array(z3.object({ from: z3.string(), to: z3.string() })).min(1) })).optional()
|
|
645
|
+
}).refine((f) => Object.values(f).some((v) => v !== void 0), "fix needs at least one change");
|
|
524
646
|
var AdvisorySchema = z3.object({
|
|
525
647
|
id: z3.string().regex(/^[a-z0-9-]+$/),
|
|
526
648
|
provider: z3.string(),
|
|
@@ -538,6 +660,8 @@ var AdvisorySchema = z3.object({
|
|
|
538
660
|
severity: z3.enum(SEVERITIES),
|
|
539
661
|
migrationNote: z3.string().min(1),
|
|
540
662
|
recipeId: z3.string().optional(),
|
|
663
|
+
/** What `recipeId: "apply-fix"` changes. */
|
|
664
|
+
fix: FixSchema.optional(),
|
|
541
665
|
sources: z3.array(z3.url()).min(1),
|
|
542
666
|
/** "verified": checked against a primary or two independent sources. "unverified": secondary reports only. */
|
|
543
667
|
status: z3.enum(["verified", "unverified"]),
|
|
@@ -573,10 +697,16 @@ function matchAdvisories(lock, advisories, options = {}) {
|
|
|
573
697
|
const horizon = options.horizonDays ?? 90;
|
|
574
698
|
const affected = [];
|
|
575
699
|
const exposed = [];
|
|
700
|
+
const possible = [];
|
|
576
701
|
const hitsPerAdvisory = /* @__PURE__ */ new Map();
|
|
577
702
|
for (const advisory of advisories) {
|
|
578
703
|
for (const callSite of lock.callSites) {
|
|
579
|
-
if (!endpointMatches(callSite, advisory))
|
|
704
|
+
if (!endpointMatches(callSite, advisory)) {
|
|
705
|
+
if (endpointMayMatch(callSite, advisory) && triggerReasons(callSite, advisory.trigger).length > 0) {
|
|
706
|
+
possible.push({ advisory, callSite, reasons: [`builds the path at runtime (${describe(callSite)}); affected if it reaches ${advisory.endpoint.path}`] });
|
|
707
|
+
}
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
580
710
|
hitsPerAdvisory.set(advisory.id, (hitsPerAdvisory.get(advisory.id) ?? 0) + 1);
|
|
581
711
|
const reasons = triggerReasons(callSite, advisory.trigger);
|
|
582
712
|
if (reasons.length > 0) affected.push({ advisory, callSite, reasons });
|
|
@@ -590,7 +720,7 @@ function matchAdvisories(lock, advisories, options = {}) {
|
|
|
590
720
|
daysUntil: Math.round((Date.parse(advisory.effectiveDate) - today) / DAY_MS),
|
|
591
721
|
callSites: hitsPerAdvisory.get(advisory.id) ?? 0
|
|
592
722
|
})).filter((u) => present.has(u.advisory.provider) && u.daysUntil >= 0 && u.daysUntil <= horizon).sort((a, b) => a.daysUntil - b.daysUntil || cmp2(a.advisory.id, b.advisory.id));
|
|
593
|
-
return { affected: affected.sort(byFinding), exposed: exposed.sort(byFinding), upcoming };
|
|
723
|
+
return { affected: affected.sort(byFinding), exposed: exposed.sort(byFinding), possible: possible.sort(byFinding), upcoming };
|
|
594
724
|
}
|
|
595
725
|
function endpointMatches(site, advisory) {
|
|
596
726
|
const pattern = advisory.endpoint;
|
|
@@ -599,6 +729,30 @@ function endpointMatches(site, advisory) {
|
|
|
599
729
|
if (pattern.hosts && !(site.host && pattern.hosts.includes(site.host))) return false;
|
|
600
730
|
return pathMatches(site.endpoint, pattern);
|
|
601
731
|
}
|
|
732
|
+
function endpointMayMatch(site, advisory) {
|
|
733
|
+
const pattern = advisory.endpoint;
|
|
734
|
+
if (site.provider !== advisory.provider || site.endpoint === null) return false;
|
|
735
|
+
if (pattern.method !== "*" && site.method !== null && site.method !== pattern.method) return false;
|
|
736
|
+
if (pattern.hosts && site.host && !pattern.hosts.includes(site.host)) return false;
|
|
737
|
+
if (!/\{[^}]*\}/.test(site.endpoint)) return false;
|
|
738
|
+
const known = segments(site.endpoint);
|
|
739
|
+
const runtimeAt = known.findIndex(isRuntime);
|
|
740
|
+
const literals = segments(pattern.path).filter((p) => !/[*{]/.test(p));
|
|
741
|
+
if (!literals.some((l) => known.slice(0, runtimeAt < 0 ? known.length : runtimeAt).includes(l))) return false;
|
|
742
|
+
return segmentsMayMatch(known, segments(pattern.path));
|
|
743
|
+
}
|
|
744
|
+
var segments = (p) => p.split("/").filter(Boolean);
|
|
745
|
+
var isRuntime = (seg) => /^\{[^}]*\}$/.test(seg);
|
|
746
|
+
function segmentsMayMatch(site, pattern) {
|
|
747
|
+
if (pattern.length === 0) return site.length === 0;
|
|
748
|
+
const [p, ...prest] = pattern;
|
|
749
|
+
if (p === "**") return segmentsMayMatch(site, prest) || site.length > 0 && segmentsMayMatch(site.slice(1), pattern);
|
|
750
|
+
if (site.length === 0) return false;
|
|
751
|
+
const [s, ...srest] = site;
|
|
752
|
+
if (isRuntime(s)) return srest.length === 0 || segmentsMayMatch(srest, prest);
|
|
753
|
+
if (!pathMatches(`/${s}`, { path: `/${p}` })) return false;
|
|
754
|
+
return segmentsMayMatch(srest, prest);
|
|
755
|
+
}
|
|
602
756
|
function pathMatches(endpoint, pattern) {
|
|
603
757
|
const sample = endpoint.replace(/\{[^}]*\}/g, "\0");
|
|
604
758
|
let re = "";
|
|
@@ -646,8 +800,12 @@ function triggerReasons(site, trigger) {
|
|
|
646
800
|
for (const q of trigger.query ?? []) {
|
|
647
801
|
const p = s.query.find((x) => x.name === q.name);
|
|
648
802
|
if (!p) continue;
|
|
649
|
-
if (q.below
|
|
650
|
-
|
|
803
|
+
if (q.below !== void 0) {
|
|
804
|
+
if (p.value !== null && Number(p.value) < q.below) reasons.push(`sends ${p.name}=${p.value} (below ${q.below})`);
|
|
805
|
+
} else if (q.values) {
|
|
806
|
+
const hit = p.value === null ? [] : p.value.split(",").map((v) => v.trim()).filter((v) => q.values.includes(v));
|
|
807
|
+
if (hit.length) reasons.push(`sends ${p.name}=${hit.join(",")}`);
|
|
808
|
+
} else reasons.push(`sends ${p.name}=${p.value ?? "(dynamic)"}`);
|
|
651
809
|
}
|
|
652
810
|
if (trigger.any) reasons.push(`calls ${describe(site)}`);
|
|
653
811
|
return reasons;
|
|
@@ -737,7 +895,11 @@ function collectConsts(ctx) {
|
|
|
737
895
|
for (const decl of ctx.sf.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
738
896
|
const list = decl.getVariableStatement()?.getDeclarationKind();
|
|
739
897
|
const init = decl.getInitializer();
|
|
740
|
-
if (list === "const" && init && Node.isIdentifier(decl.getNameNode()))
|
|
898
|
+
if (list === "const" && init && Node.isIdentifier(decl.getNameNode())) {
|
|
899
|
+
ctx.consts.set(decl.getName(), init);
|
|
900
|
+
ctx.declared ??= /* @__PURE__ */ new Map();
|
|
901
|
+
ctx.declared.set(decl.getName(), [...ctx.declared.get(decl.getName()) ?? [], init]);
|
|
902
|
+
}
|
|
741
903
|
}
|
|
742
904
|
}
|
|
743
905
|
function collectClients(ctx) {
|
|
@@ -843,7 +1005,8 @@ function fetchStyle(ctx, call, lib, urlArg, optsArg, verb, baseURL) {
|
|
|
843
1005
|
if (opts) method = methodFrom(ctx, propertyValue(opts, "method")) ?? (propertyValue(opts, "method") ? null : "GET");
|
|
844
1006
|
else method = null;
|
|
845
1007
|
}
|
|
846
|
-
|
|
1008
|
+
const params = opts && (lib === "got" || lib === "ky") ? resolveObject(ctx, propertyValue(opts, "searchParams")) : void 0;
|
|
1009
|
+
return httpSite(ctx, call, lib, joinUrl(baseURL, evalString(ctx, urlArg)), method, opts ? optsArg : void 0, params);
|
|
847
1010
|
}
|
|
848
1011
|
function axiosStyle(ctx, call, verb, baseURL) {
|
|
849
1012
|
const args = call.getArguments();
|
|
@@ -869,7 +1032,7 @@ function httpSite(ctx, call, lib, url, method, options, params) {
|
|
|
869
1032
|
const surface = emptySurface();
|
|
870
1033
|
surface.headers = headersOf(ctx, options);
|
|
871
1034
|
surface.query = [...query, ...objectEntries(ctx, params)];
|
|
872
|
-
const provider = host
|
|
1035
|
+
const provider = providerOf(host, path13);
|
|
873
1036
|
return {
|
|
874
1037
|
call,
|
|
875
1038
|
options,
|
|
@@ -1051,9 +1214,16 @@ function evalString(ctx, node, depth = 0) {
|
|
|
1051
1214
|
}
|
|
1052
1215
|
}
|
|
1053
1216
|
if (Node.isIdentifier(expr)) {
|
|
1054
|
-
const init = ctx.consts.get(expr.getText());
|
|
1217
|
+
const init = nearestConst(ctx, expr) ?? ctx.consts.get(expr.getText());
|
|
1055
1218
|
return init && init !== node ? evalString(ctx, init, depth + 1) : void 0;
|
|
1056
1219
|
}
|
|
1220
|
+
if (Node.isCallExpression(expr)) {
|
|
1221
|
+
const callee = expr.getExpression();
|
|
1222
|
+
const args = expr.getArguments();
|
|
1223
|
+
if (Node.isPropertyAccessExpression(callee) && callee.getName() === "toString" && args.length === 0) return evalString(ctx, callee.getExpression(), depth + 1);
|
|
1224
|
+
if (Node.isIdentifier(callee) && callee.getText() === "String" && args.length === 1) return evalString(ctx, args[0], depth + 1);
|
|
1225
|
+
}
|
|
1226
|
+
if (Node.isPropertyAccessExpression(expr) && expr.getName() === "href") return evalString(ctx, expr.getExpression(), depth + 1);
|
|
1057
1227
|
if (Node.isNewExpression(expr) && expr.getExpression().getText() === "URL") {
|
|
1058
1228
|
const [p, base] = expr.getArguments();
|
|
1059
1229
|
const pathPart = evalString(ctx, p, depth + 1);
|
|
@@ -1062,6 +1232,18 @@ function evalString(ctx, node, depth = 0) {
|
|
|
1062
1232
|
}
|
|
1063
1233
|
return void 0;
|
|
1064
1234
|
}
|
|
1235
|
+
function nearestConst(ctx, id) {
|
|
1236
|
+
const inits = ctx.declared?.get(id.getText());
|
|
1237
|
+
if (!inits || inits.length < 2) return void 0;
|
|
1238
|
+
let best;
|
|
1239
|
+
for (const init of inits) {
|
|
1240
|
+
const scope = init.getFirstAncestor((a) => Node.isBlock(a) || Node.isSourceFile(a));
|
|
1241
|
+
if (!scope || id.getPos() < scope.getPos() || id.getEnd() > scope.getEnd()) continue;
|
|
1242
|
+
const size = scope.getEnd() - scope.getPos();
|
|
1243
|
+
if (!best || size < best.size) best = { init, size };
|
|
1244
|
+
}
|
|
1245
|
+
return best?.init;
|
|
1246
|
+
}
|
|
1065
1247
|
function placeholder(node) {
|
|
1066
1248
|
const expr = unwrap(node);
|
|
1067
1249
|
if (Node.isIdentifier(expr)) return `{${expr.getText()}}`;
|
|
@@ -1069,6 +1251,53 @@ function placeholder(node) {
|
|
|
1069
1251
|
if (Node.isCallExpression(expr) && expr.getArguments().length === 1) return placeholder(expr.getArguments()[0]);
|
|
1070
1252
|
return "{}";
|
|
1071
1253
|
}
|
|
1254
|
+
function providerOf(host, path13) {
|
|
1255
|
+
if (host) return providerForHost(host)?.id ?? null;
|
|
1256
|
+
return path13 ? providerForPath(path13)?.id ?? null : null;
|
|
1257
|
+
}
|
|
1258
|
+
function urlParts(value2) {
|
|
1259
|
+
const v = value2.trim().replace(/\{\{\s*([^{}]*?)\s*\}\}/g, "{$1}");
|
|
1260
|
+
if (/^(?:https?:)?\/\//i.test(v)) return parseUrl(v);
|
|
1261
|
+
const asPath = "/" + v.replace(/^\{[^}]*\}(?=\/)/, "").replace(/^\/+/, "");
|
|
1262
|
+
if (providerForPath(asPath.split(/[?#]/)[0])) return parseUrl(asPath);
|
|
1263
|
+
return void 0;
|
|
1264
|
+
}
|
|
1265
|
+
function looseUrlSites(sf, file, found) {
|
|
1266
|
+
const ctx = { sf, imports: /* @__PURE__ */ new Map(), consts: /* @__PURE__ */ new Map(), http: /* @__PURE__ */ new Map(), sdk: /* @__PURE__ */ new Map() };
|
|
1267
|
+
collectConsts(ctx);
|
|
1268
|
+
const covered = new Set(found.map((f) => `${f.site.provider} ${f.site.endpoint}`));
|
|
1269
|
+
const inFoundCall = (node) => found.some((f) => node.getPos() >= f.call.getPos() && node.getEnd() <= f.call.getEnd());
|
|
1270
|
+
const candidates = [];
|
|
1271
|
+
for (const node of sf.getDescendants()) {
|
|
1272
|
+
if (!Node.isStringLiteral(node) && !Node.isNoSubstitutionTemplateLiteral(node) && !Node.isTemplateExpression(node) && !isConcat(node)) continue;
|
|
1273
|
+
const parent = node.getParent();
|
|
1274
|
+
if (parent && (Node.isTemplateSpan(parent) || isConcat(parent) || Node.isImportDeclaration(parent) || Node.isExportDeclaration(parent))) continue;
|
|
1275
|
+
if (parent && Node.isPropertyAssignment(parent) && parent.getNameNode() === node) continue;
|
|
1276
|
+
if (inFoundCall(node)) continue;
|
|
1277
|
+
const value2 = evalString(ctx, node);
|
|
1278
|
+
const parts = value2 === void 0 ? void 0 : urlParts(value2);
|
|
1279
|
+
if (!parts?.path || parts.path === "/") continue;
|
|
1280
|
+
const provider = providerOf(parts.host, parts.path);
|
|
1281
|
+
if (provider) candidates.push({ node, host: parts.host, path: parts.path, query: parts.query, provider });
|
|
1282
|
+
}
|
|
1283
|
+
const sites = [];
|
|
1284
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1285
|
+
for (const c of candidates) {
|
|
1286
|
+
const key = `${c.provider} ${c.path}`;
|
|
1287
|
+
if (covered.has(key) || seen.has(key)) continue;
|
|
1288
|
+
if (candidates.some((o) => o.provider === c.provider && o.path !== c.path && o.path.startsWith(`${c.path}/`))) continue;
|
|
1289
|
+
if (found.some((f) => f.site.provider === c.provider && f.site.endpoint?.startsWith(`${c.path}/`))) continue;
|
|
1290
|
+
seen.add(key);
|
|
1291
|
+
const { line, column } = sf.getLineAndColumnAtPos(c.node.getStart());
|
|
1292
|
+
const surface = emptySurface();
|
|
1293
|
+
surface.query = c.query;
|
|
1294
|
+
sites.push({ file, line, column, provider: c.provider, kind: "http", client: "url", method: null, host: c.host, endpoint: c.path, resolved: true, surface });
|
|
1295
|
+
}
|
|
1296
|
+
return sites;
|
|
1297
|
+
}
|
|
1298
|
+
function isConcat(node) {
|
|
1299
|
+
return Node.isBinaryExpression(node) && node.getOperatorToken().getKind() === SyntaxKind.PlusToken;
|
|
1300
|
+
}
|
|
1072
1301
|
function joinUrl(base, url) {
|
|
1073
1302
|
if (url === void 0) return void 0;
|
|
1074
1303
|
if (!base || /^[a-z]+:\/\//i.test(url)) return url;
|
|
@@ -1136,6 +1365,7 @@ var NOT_FIELDS = /* @__PURE__ */ new Set([
|
|
|
1136
1365
|
"valueOf",
|
|
1137
1366
|
"toJSON"
|
|
1138
1367
|
]);
|
|
1368
|
+
var ITEM_CALLBACKS = { map: 0, flatMap: 0, filter: 0, forEach: 0, find: 0, findIndex: 0, some: 0, every: 0, reduce: 1 };
|
|
1139
1369
|
var MAX_DEPTH = 4;
|
|
1140
1370
|
var Collector = class {
|
|
1141
1371
|
fields = /* @__PURE__ */ new Set();
|
|
@@ -1250,7 +1480,7 @@ function use(ref, loc, env, c, depth) {
|
|
|
1250
1480
|
function interpret(env, loc, access, top, c, depth, compare) {
|
|
1251
1481
|
const full = [...loc.path, ...access];
|
|
1252
1482
|
if (loc.space === "body" || env === "body") {
|
|
1253
|
-
body(full, top, c, compare);
|
|
1483
|
+
body(full, top, c, compare, env, depth);
|
|
1254
1484
|
return;
|
|
1255
1485
|
}
|
|
1256
1486
|
const [first, ...rest] = full;
|
|
@@ -1276,20 +1506,37 @@ function interpret(env, loc, access, top, c, depth, compare) {
|
|
|
1276
1506
|
} else if (h && h !== "get") c.responseHeaders.add(h.toLowerCase());
|
|
1277
1507
|
return;
|
|
1278
1508
|
}
|
|
1279
|
-
if (env === "axios" && first === "data") return body(rest, top, c, compare);
|
|
1280
|
-
if (env === "got" && first === "body") return body(rest, top, c, compare);
|
|
1509
|
+
if (env === "axios" && first === "data") return body(rest, top, c, compare, env, depth);
|
|
1510
|
+
if (env === "got" && first === "body") return body(rest, top, c, compare, env, depth);
|
|
1281
1511
|
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
1512
|
if (bodyCall && isCall()) {
|
|
1283
1513
|
bindFrom(top.getParent(), { space: "body", path: [] }, env, c, depth + 1);
|
|
1284
1514
|
}
|
|
1285
1515
|
}
|
|
1286
|
-
function body(path13, top, c, compare) {
|
|
1516
|
+
function body(path13, top, c, compare, env, depth) {
|
|
1287
1517
|
const cut = path13.findIndex((p) => NOT_FIELDS.has(p));
|
|
1288
1518
|
const fieldPath = cut >= 0 ? path13.slice(0, cut) : path13;
|
|
1289
1519
|
if (fieldPath.length === 0) return;
|
|
1290
1520
|
const field = fieldPath.join(".").replace(/\.\[\]/g, "[]");
|
|
1291
1521
|
c.fields.add(field);
|
|
1292
1522
|
if (compare && cut < 0) for (const s of comparedLiterals(top).strs) c.addEnum(field, s);
|
|
1523
|
+
if (depth >= MAX_DEPTH) return;
|
|
1524
|
+
const items = { space: "body", path: [...fieldPath, "[]"] };
|
|
1525
|
+
const method = cut >= 0 && cut === path13.length - 1 ? path13[cut] : void 0;
|
|
1526
|
+
const call = top.getParent();
|
|
1527
|
+
if (method && method in ITEM_CALLBACKS && Node2.isCallExpression(call) && call.getExpression() === top) {
|
|
1528
|
+
const cb = call.getArguments()[0];
|
|
1529
|
+
const param = cb && (Node2.isArrowFunction(cb) || Node2.isFunctionExpression(cb)) ? cb.getParameters()[ITEM_CALLBACKS[method]] : void 0;
|
|
1530
|
+
if (param) bindName(param.getNameNode(), items, param, env, c, depth + 1);
|
|
1531
|
+
if (method === "find") bindFrom(call, items, env, c, depth + 1);
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
const loop = top.getParent();
|
|
1535
|
+
if (cut < 0 && Node2.isForOfStatement(loop) && loop.getExpression() === top) {
|
|
1536
|
+
const list = loop.getInitializer();
|
|
1537
|
+
const decl = Node2.isVariableDeclarationList(list) ? list.getDeclarations()[0] : void 0;
|
|
1538
|
+
if (decl) bindName(decl.getNameNode(), items, decl, env, c, depth + 1);
|
|
1539
|
+
}
|
|
1293
1540
|
}
|
|
1294
1541
|
function comparedLiterals(node) {
|
|
1295
1542
|
const nums = [];
|
|
@@ -1323,10 +1570,14 @@ function comparedLiterals(node) {
|
|
|
1323
1570
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
1324
1571
|
import path3 from "path";
|
|
1325
1572
|
var SOURCE_FILE = /\.(c|m)?[jt]sx?$/;
|
|
1326
|
-
var
|
|
1573
|
+
var PYTHON_FILE = /\.py$/;
|
|
1574
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "vendor", "bower_components", "venv", "__pycache__", "site-packages"]);
|
|
1575
|
+
var PYTHON_MANIFEST = /^(requirements[\w.-]*\.txt|pyproject\.toml)$/;
|
|
1327
1576
|
function listRepoFiles(root) {
|
|
1328
1577
|
const sources = [];
|
|
1329
1578
|
const manifests = [];
|
|
1579
|
+
const python = [];
|
|
1580
|
+
const pythonManifests = [];
|
|
1330
1581
|
const walk2 = (dir) => {
|
|
1331
1582
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
1332
1583
|
if (entry.name.startsWith(".")) continue;
|
|
@@ -1336,11 +1587,13 @@ function listRepoFiles(root) {
|
|
|
1336
1587
|
} else if (entry.isFile()) {
|
|
1337
1588
|
if (entry.name === "package.json") manifests.push(full);
|
|
1338
1589
|
else if (SOURCE_FILE.test(entry.name) && !entry.name.endsWith(".d.ts")) sources.push(full);
|
|
1590
|
+
else if (PYTHON_FILE.test(entry.name)) python.push(full);
|
|
1591
|
+
else if (PYTHON_MANIFEST.test(entry.name)) pythonManifests.push(full);
|
|
1339
1592
|
}
|
|
1340
1593
|
}
|
|
1341
1594
|
};
|
|
1342
1595
|
walk2(root);
|
|
1343
|
-
return { sources: sources.sort(), manifests: manifests.sort() };
|
|
1596
|
+
return { sources: sources.sort(), manifests: manifests.sort(), python: python.sort(), pythonManifests: pythonManifests.sort() };
|
|
1344
1597
|
}
|
|
1345
1598
|
function declaredProviders(root, manifests) {
|
|
1346
1599
|
const found = [];
|
|
@@ -1366,6 +1619,578 @@ function declaredProviders(root, manifests) {
|
|
|
1366
1619
|
function toPosix(p) {
|
|
1367
1620
|
return p.split(path3.sep).join("/");
|
|
1368
1621
|
}
|
|
1622
|
+
function declaredPythonProviders(root, manifests) {
|
|
1623
|
+
const found = [];
|
|
1624
|
+
for (const manifest of manifests) {
|
|
1625
|
+
let text;
|
|
1626
|
+
try {
|
|
1627
|
+
text = readFileSync2(manifest, "utf8");
|
|
1628
|
+
} catch {
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
const names = /* @__PURE__ */ new Set();
|
|
1632
|
+
if (manifest.endsWith(".txt")) {
|
|
1633
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1634
|
+
const m = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(line.replace(/#.*/, ""));
|
|
1635
|
+
if (m && !line.trim().startsWith("-")) names.add(m[1]);
|
|
1636
|
+
}
|
|
1637
|
+
} else {
|
|
1638
|
+
let inTable = false;
|
|
1639
|
+
let inList = false;
|
|
1640
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1641
|
+
const t = line.replace(/#.*/, "").trim();
|
|
1642
|
+
if (/^\[/.test(t)) {
|
|
1643
|
+
inTable = /dependencies\]$/.test(t);
|
|
1644
|
+
inList = false;
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
if (/^(dependencies|optional-dependencies|dev-dependencies)\s*=\s*\[/.test(t) || /^[\w-]+\s*=\s*\[/.test(t)) inList = true;
|
|
1648
|
+
if (inList) for (const m of t.matchAll(/["']([A-Za-z0-9][A-Za-z0-9._-]*)/g)) names.add(m[1]);
|
|
1649
|
+
if (inList && t.includes("]")) inList = false;
|
|
1650
|
+
const kv = inTable ? /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*=/.exec(t) : null;
|
|
1651
|
+
if (kv) names.add(kv[1]);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
for (const name of [...names].sort()) {
|
|
1655
|
+
const provider = providerForPythonPackage(name);
|
|
1656
|
+
if (provider) found.push({ provider: provider.id, package: name, manifest: toPosix(path3.relative(root, manifest)) });
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
return found;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// packages/extractor/src/python.ts
|
|
1663
|
+
import { existsSync } from "fs";
|
|
1664
|
+
import { createRequire } from "module";
|
|
1665
|
+
import { fileURLToPath } from "url";
|
|
1666
|
+
import { Language, Parser } from "web-tree-sitter";
|
|
1667
|
+
var parser;
|
|
1668
|
+
async function loadPythonParser() {
|
|
1669
|
+
if (parser) return;
|
|
1670
|
+
await Parser.init();
|
|
1671
|
+
const bundled = fileURLToPath(new URL("./tree-sitter-python.wasm", import.meta.url));
|
|
1672
|
+
const wasm = existsSync(bundled) ? bundled : createRequire(import.meta.url).resolve("tree-sitter-python/tree-sitter-python.wasm");
|
|
1673
|
+
const language = await Language.load(wasm);
|
|
1674
|
+
const p = new Parser();
|
|
1675
|
+
p.setLanguage(language);
|
|
1676
|
+
parser = p;
|
|
1677
|
+
}
|
|
1678
|
+
function pythonParserLoaded() {
|
|
1679
|
+
return parser !== void 0;
|
|
1680
|
+
}
|
|
1681
|
+
var HTTP_LIBS = /* @__PURE__ */ new Set(["requests", "httpx", "aiohttp"]);
|
|
1682
|
+
var VERBS2 = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
1683
|
+
function extractPython(source, file) {
|
|
1684
|
+
if (!parser) throw new Error("call loadPythonParser() before extracting Python");
|
|
1685
|
+
const tree = parser.parse(source);
|
|
1686
|
+
if (!tree) return [];
|
|
1687
|
+
try {
|
|
1688
|
+
const root = tree.rootNode;
|
|
1689
|
+
const ctx = { names: /* @__PURE__ */ new Map(), consts: /* @__PURE__ */ new Map() };
|
|
1690
|
+
collectImports2(root, ctx);
|
|
1691
|
+
collectConsts2(root, ctx);
|
|
1692
|
+
collectClients2(root, ctx);
|
|
1693
|
+
const sites = [];
|
|
1694
|
+
const calls = [];
|
|
1695
|
+
for (const call of root.descendantsOfType("call")) {
|
|
1696
|
+
if (!call) continue;
|
|
1697
|
+
const site = httpCall(ctx, call) ?? sdkCall(ctx, call);
|
|
1698
|
+
if (!site) continue;
|
|
1699
|
+
site.file = file;
|
|
1700
|
+
site.line = call.startPosition.row + 1;
|
|
1701
|
+
site.column = call.startPosition.column + 1;
|
|
1702
|
+
addResponseSurface2(call, site);
|
|
1703
|
+
sites.push(site);
|
|
1704
|
+
calls.push(call);
|
|
1705
|
+
}
|
|
1706
|
+
return [...sites, ...looseUrls(ctx, root, file, sites, calls)];
|
|
1707
|
+
} finally {
|
|
1708
|
+
tree.delete();
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
function collectImports2(root, ctx) {
|
|
1712
|
+
for (const st of root.descendantsOfType(["import_statement", "import_from_statement"])) {
|
|
1713
|
+
if (!st) continue;
|
|
1714
|
+
if (st.type === "import_statement") {
|
|
1715
|
+
for (const item of st.namedChildren) {
|
|
1716
|
+
if (!item) continue;
|
|
1717
|
+
const dotted = item.type === "aliased_import" ? item.childForFieldName("name") : item;
|
|
1718
|
+
const alias = item.type === "aliased_import" ? item.childForFieldName("alias")?.text : dotted?.text.split(".")[0];
|
|
1719
|
+
if (!dotted || !alias) continue;
|
|
1720
|
+
bindModule(ctx, alias, dotted.text);
|
|
1721
|
+
}
|
|
1722
|
+
} else {
|
|
1723
|
+
const module = st.childForFieldName("module_name")?.text;
|
|
1724
|
+
if (!module) continue;
|
|
1725
|
+
for (const item of st.childrenForFieldName("name")) {
|
|
1726
|
+
if (!item) continue;
|
|
1727
|
+
const name = item.type === "aliased_import" ? item.childForFieldName("name")?.text : item.text;
|
|
1728
|
+
const alias = item.type === "aliased_import" ? item.childForFieldName("alias")?.text : item.text;
|
|
1729
|
+
if (name && alias) bindModule(ctx, alias, `${module}.${name}`);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
function bindModule(ctx, alias, dotted) {
|
|
1735
|
+
const top = dotted.split(".")[0];
|
|
1736
|
+
if (HTTP_LIBS.has(top)) {
|
|
1737
|
+
ctx.names.set(alias, { kind: "http-module", lib: top });
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
const provider = providerForPythonModule(top);
|
|
1741
|
+
if (provider) ctx.names.set(alias, { kind: "sdk-module", provider, module: dotted });
|
|
1742
|
+
}
|
|
1743
|
+
function collectConsts2(root, ctx) {
|
|
1744
|
+
for (const st of root.namedChildren) {
|
|
1745
|
+
const assign = st?.type === "expression_statement" ? st.namedChildren[0] : void 0;
|
|
1746
|
+
if (assign?.type !== "assignment") continue;
|
|
1747
|
+
const left = assign.childForFieldName("left");
|
|
1748
|
+
const right = assign.childForFieldName("right");
|
|
1749
|
+
if (left?.type === "identifier" && right) ctx.consts.set(left.text, right);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
function collectClients2(root, ctx) {
|
|
1753
|
+
const visit = (target, value2) => {
|
|
1754
|
+
const call = unwrap2(value2);
|
|
1755
|
+
if (call.type !== "call") return;
|
|
1756
|
+
const fn = call.childForFieldName("function");
|
|
1757
|
+
const chain = fn ? chainOf2(fn) : void 0;
|
|
1758
|
+
if (!chain) return;
|
|
1759
|
+
const base = ctx.names.get(chain.root);
|
|
1760
|
+
if (!base) return;
|
|
1761
|
+
if (base.kind === "http-module" || base.kind === "http-client") {
|
|
1762
|
+
const ctor = chain.names.at(-1);
|
|
1763
|
+
if (!ctor || !/^(Session|Client|AsyncClient|ClientSession)$/.test(ctor)) return;
|
|
1764
|
+
const args = call.childForFieldName("arguments");
|
|
1765
|
+
const baseUrl = evalString2(ctx, keyword(args, "base_url"));
|
|
1766
|
+
ctx.names.set(target, { kind: "http-client", lib: base.lib, ...baseUrl ? { baseUrl } : {}, headers: dictEntries(ctx, keyword(args, "headers")) });
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
if (base.kind === "sdk-module" && !sdkEndpoint2(base.provider, [...modulePath(base), ...chain.names])) {
|
|
1770
|
+
ctx.names.set(target, { kind: "sdk-client", provider: base.provider, module: base.module });
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
for (const assign of root.descendantsOfType("assignment")) {
|
|
1774
|
+
const left = assign?.childForFieldName("left");
|
|
1775
|
+
const right = assign?.childForFieldName("right");
|
|
1776
|
+
if (left && right && (left.type === "identifier" || left.type === "attribute")) visit(left.text, right);
|
|
1777
|
+
}
|
|
1778
|
+
for (const item of root.descendantsOfType("as_pattern")) {
|
|
1779
|
+
const value2 = item?.namedChildren[0];
|
|
1780
|
+
const alias = item?.childForFieldName("alias")?.namedChildren[0] ?? item?.namedChildren.at(-1);
|
|
1781
|
+
if (value2 && alias && alias.type === "identifier") visit(alias.text, value2);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
function httpCall(ctx, call) {
|
|
1785
|
+
const fn = call.childForFieldName("function");
|
|
1786
|
+
const chain = fn ? chainOf2(fn) : void 0;
|
|
1787
|
+
if (!chain || chain.names.length === 0) {
|
|
1788
|
+
return void 0;
|
|
1789
|
+
}
|
|
1790
|
+
const binding = ctx.names.get(chain.root) ?? ctx.names.get(chain.prefix(chain.names.length - 1));
|
|
1791
|
+
const member = chain.names.at(-1);
|
|
1792
|
+
if (!binding || binding.kind !== "http-module" && binding.kind !== "http-client") return void 0;
|
|
1793
|
+
if (binding.kind === "http-module" && chain.names.length !== 1) return void 0;
|
|
1794
|
+
const args = call.childForFieldName("arguments");
|
|
1795
|
+
const positional = (args?.namedChildren ?? []).filter((a) => a && a.type !== "keyword_argument");
|
|
1796
|
+
let method;
|
|
1797
|
+
let urlNode;
|
|
1798
|
+
if (VERBS2.has(member)) {
|
|
1799
|
+
method = member.toUpperCase();
|
|
1800
|
+
urlNode = positional[0] ?? keyword(args, "url");
|
|
1801
|
+
} else if (member === "request") {
|
|
1802
|
+
const m = evalString2(ctx, positional[0] ?? keyword(args, "method"))?.toUpperCase();
|
|
1803
|
+
method = m && HTTP_METHODS.includes(m) ? m : null;
|
|
1804
|
+
urlNode = positional[1] ?? keyword(args, "url");
|
|
1805
|
+
} else return void 0;
|
|
1806
|
+
const base = binding.kind === "http-client" ? binding.baseUrl : void 0;
|
|
1807
|
+
const { host, path: path13, query } = parseUrl(joinUrl2(base, evalString2(ctx, urlNode)));
|
|
1808
|
+
const surface = emptySurface2();
|
|
1809
|
+
surface.headers = [...binding.kind === "http-client" ? binding.headers : [], ...dictEntries(ctx, keyword(args, "headers"))].map((h) => ({ ...h, name: h.name.toLowerCase() }));
|
|
1810
|
+
surface.query = [...query, ...dictEntries(ctx, keyword(args, "params"))];
|
|
1811
|
+
const provider = providerOf(host, path13);
|
|
1812
|
+
return { file: "", line: 0, column: 0, provider, kind: "http", client: binding.lib, method, host, endpoint: path13, resolved: Boolean(provider && path13 && method), surface };
|
|
1813
|
+
}
|
|
1814
|
+
function sdkCall(ctx, call) {
|
|
1815
|
+
const fn = call.childForFieldName("function");
|
|
1816
|
+
const chain = fn ? chainOf2(fn) : void 0;
|
|
1817
|
+
if (!chain || chain.names.length === 0) return void 0;
|
|
1818
|
+
const binding = ctx.names.get(chain.root);
|
|
1819
|
+
if (!binding || binding.kind !== "sdk-module" && binding.kind !== "sdk-client") return void 0;
|
|
1820
|
+
const names = binding.kind === "sdk-module" ? [...modulePath(binding), ...chain.names] : chain.names;
|
|
1821
|
+
const ep = sdkEndpoint2(binding.provider, names);
|
|
1822
|
+
if (!ep && (binding.kind === "sdk-module" || names.length < 2)) return void 0;
|
|
1823
|
+
const surface = emptySurface2();
|
|
1824
|
+
const host = binding.provider.hosts.find((h) => !h.startsWith("*.")) ?? null;
|
|
1825
|
+
return {
|
|
1826
|
+
file: "",
|
|
1827
|
+
line: 0,
|
|
1828
|
+
column: 0,
|
|
1829
|
+
provider: binding.provider.id,
|
|
1830
|
+
kind: "sdk",
|
|
1831
|
+
client: binding.module.split(".")[0],
|
|
1832
|
+
operation: names.join("."),
|
|
1833
|
+
method: ep?.method ?? null,
|
|
1834
|
+
host,
|
|
1835
|
+
endpoint: ep?.path ?? null,
|
|
1836
|
+
resolved: Boolean(ep),
|
|
1837
|
+
surface
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
function modulePath(b) {
|
|
1841
|
+
return b.module.split(".").slice(1);
|
|
1842
|
+
}
|
|
1843
|
+
function sdkEndpoint2(provider, names) {
|
|
1844
|
+
let op = names.join(".");
|
|
1845
|
+
if (provider.id === "stripe") op = op.replace(/^v1\./, "");
|
|
1846
|
+
if (provider.id === "slack") {
|
|
1847
|
+
const last = names.at(-1);
|
|
1848
|
+
if (names.length !== 1 || !last || !/^[a-z]+(_[a-zA-Z0-9]+)+$/.test(last)) return void 0;
|
|
1849
|
+
if (last === "files_upload_v2") return { method: "POST", path: "/api/files.completeUploadExternal" };
|
|
1850
|
+
return { method: "POST", path: `/api/${last.replace(/_/g, ".")}` };
|
|
1851
|
+
}
|
|
1852
|
+
const ref = getProvider(provider.id)?.python?.methods?.[op];
|
|
1853
|
+
if (!ref) return void 0;
|
|
1854
|
+
const [method, path13] = ref.split(" ");
|
|
1855
|
+
return { method, path: path13 };
|
|
1856
|
+
}
|
|
1857
|
+
var NOT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
1858
|
+
"keys",
|
|
1859
|
+
"values",
|
|
1860
|
+
"items",
|
|
1861
|
+
"append",
|
|
1862
|
+
"extend",
|
|
1863
|
+
"lower",
|
|
1864
|
+
"upper",
|
|
1865
|
+
"strip",
|
|
1866
|
+
"split",
|
|
1867
|
+
"startswith",
|
|
1868
|
+
"endswith",
|
|
1869
|
+
"replace",
|
|
1870
|
+
"format",
|
|
1871
|
+
"encode",
|
|
1872
|
+
"decode",
|
|
1873
|
+
"count",
|
|
1874
|
+
"index",
|
|
1875
|
+
"copy",
|
|
1876
|
+
"pop",
|
|
1877
|
+
"update",
|
|
1878
|
+
"sort"
|
|
1879
|
+
]);
|
|
1880
|
+
var SAME_DATA = /* @__PURE__ */ new Set(["to_dict", "to_dict_recursive", "model_dump", "dict"]);
|
|
1881
|
+
var MAX_DEPTH2 = 4;
|
|
1882
|
+
var Collector2 = class {
|
|
1883
|
+
fields = /* @__PURE__ */ new Set();
|
|
1884
|
+
statusCodes = /* @__PURE__ */ new Set();
|
|
1885
|
+
checksOk = false;
|
|
1886
|
+
enums = /* @__PURE__ */ new Map();
|
|
1887
|
+
responseHeaders = /* @__PURE__ */ new Set();
|
|
1888
|
+
};
|
|
1889
|
+
function addResponseSurface2(call, site) {
|
|
1890
|
+
const c = new Collector2();
|
|
1891
|
+
const envelope = site.kind === "sdk" ? "body" : "response";
|
|
1892
|
+
bindFrom2(call, { space: envelope, path: [] }, site.client, c, 0);
|
|
1893
|
+
const s = site.surface;
|
|
1894
|
+
s.fields = [...c.fields].sort();
|
|
1895
|
+
s.statusCodes = [...c.statusCodes].sort((a, b) => a - b);
|
|
1896
|
+
s.checksOk = c.checksOk;
|
|
1897
|
+
s.enums = [...c.enums].map(([field, values]) => ({ field, values: [...values].sort() })).sort((a, b) => a.field.localeCompare(b.field));
|
|
1898
|
+
s.responseHeaders = [...c.responseHeaders].sort();
|
|
1899
|
+
}
|
|
1900
|
+
function bindFrom2(expr, loc, lib, c, depth) {
|
|
1901
|
+
if (depth > MAX_DEPTH2) return;
|
|
1902
|
+
let cur = expr;
|
|
1903
|
+
while (cur.parent && (cur.parent.type === "await" || cur.parent.type === "parenthesized_expression")) cur = cur.parent;
|
|
1904
|
+
const parent = cur.parent;
|
|
1905
|
+
if (!parent) return;
|
|
1906
|
+
if (parent.type === "assignment" && parent.childForFieldName("right")?.id === cur.id) {
|
|
1907
|
+
const left = parent.childForFieldName("left");
|
|
1908
|
+
if (left?.type === "identifier") bindName2(left.text, parent, loc, lib, c, depth);
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
if (parent.type === "as_pattern" && parent.namedChildren[0]?.id === cur.id) {
|
|
1912
|
+
const alias = parent.childForFieldName("alias")?.namedChildren[0] ?? parent.namedChildren.at(-1);
|
|
1913
|
+
if (alias?.type === "identifier") bindName2(alias.text, parent, loc, lib, c, depth);
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
use2(cur, loc, lib, c, depth);
|
|
1917
|
+
}
|
|
1918
|
+
function bindName2(name, decl, loc, lib, c, depth) {
|
|
1919
|
+
for (const ref of referencesOf2(name, decl)) use2(ref, loc, lib, c, depth);
|
|
1920
|
+
}
|
|
1921
|
+
function referencesOf2(name, decl) {
|
|
1922
|
+
let scope = decl.parent;
|
|
1923
|
+
while (scope && scope.type !== "function_definition" && scope.type !== "lambda" && scope.type !== "module") scope = scope.parent;
|
|
1924
|
+
if (!scope) return [];
|
|
1925
|
+
return scope.descendantsOfType("identifier").filter((id) => {
|
|
1926
|
+
if (!id || id.text !== name || id.startIndex < decl.endIndex) return false;
|
|
1927
|
+
const p = id.parent;
|
|
1928
|
+
if (p?.type === "attribute" && p.childForFieldName("attribute")?.id === id.id) return false;
|
|
1929
|
+
if (p?.type === "keyword_argument" && p.childForFieldName("name")?.id === id.id) return false;
|
|
1930
|
+
if (p?.type === "assignment" && p.childForFieldName("left")?.id === id.id) return false;
|
|
1931
|
+
return true;
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
function use2(ref, loc, lib, c, depth) {
|
|
1935
|
+
const access = [];
|
|
1936
|
+
let cur = ref;
|
|
1937
|
+
for (; ; ) {
|
|
1938
|
+
const p = cur.parent;
|
|
1939
|
+
if (!p) break;
|
|
1940
|
+
if (p.type === "attribute" && p.childForFieldName("object")?.id === cur.id) {
|
|
1941
|
+
const name = p.childForFieldName("attribute")?.text ?? "";
|
|
1942
|
+
const call = p.parent?.type === "call" && p.parent.childForFieldName("function")?.id === p.id ? p.parent : void 0;
|
|
1943
|
+
if (call && name === "get" && (loc.space === "body" || access.length > 0)) {
|
|
1944
|
+
const key = stringValue(call.childForFieldName("arguments")?.namedChildren[0] ?? void 0);
|
|
1945
|
+
if (key === void 0) break;
|
|
1946
|
+
access.push(key);
|
|
1947
|
+
cur = call;
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
if (call && SAME_DATA.has(name) && loc.space === "body") {
|
|
1951
|
+
cur = call;
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
access.push(name);
|
|
1955
|
+
cur = p;
|
|
1956
|
+
} else if (p.type === "subscript" && p.childForFieldName("value")?.id === cur.id) {
|
|
1957
|
+
const key = stringValue(p.childForFieldName("subscript") ?? void 0);
|
|
1958
|
+
access.push(key ?? "[]");
|
|
1959
|
+
cur = p;
|
|
1960
|
+
} else if (p.type === "parenthesized_expression" || p.type === "await") {
|
|
1961
|
+
cur = p;
|
|
1962
|
+
} else break;
|
|
1963
|
+
}
|
|
1964
|
+
interpret2(loc, access, cur, lib, c, depth);
|
|
1965
|
+
}
|
|
1966
|
+
function interpret2(loc, access, top, lib, c, depth) {
|
|
1967
|
+
const full = [...loc.path, ...access];
|
|
1968
|
+
if (loc.space === "body") return body2(full, top, lib, c, depth);
|
|
1969
|
+
const [first, ...rest] = full;
|
|
1970
|
+
if (first === void 0) return;
|
|
1971
|
+
const isCall = top.parent?.type === "call" && top.parent.childForFieldName("function")?.id === top.id;
|
|
1972
|
+
const statusKey = lib === "aiohttp" ? "status" : "status_code";
|
|
1973
|
+
if (first === statusKey && rest.length === 0) {
|
|
1974
|
+
for (const n of comparedLiterals2(top).nums) c.statusCodes.add(n);
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
if (first === "ok" && rest.length === 0 || first === "raise_for_status" && isCall) {
|
|
1978
|
+
c.checksOk = true;
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
if (first === "headers") {
|
|
1982
|
+
if (rest[0]) c.responseHeaders.add(rest[0].toLowerCase());
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
if (first === "json" && rest.length === 0 && isCall) bindFrom2(top.parent, { space: "body", path: [] }, lib, c, depth + 1);
|
|
1986
|
+
}
|
|
1987
|
+
function body2(path13, top, lib, c, depth) {
|
|
1988
|
+
const cut = path13.findIndex((p) => NOT_FIELDS2.has(p));
|
|
1989
|
+
const fieldPath = cut >= 0 ? path13.slice(0, cut) : path13;
|
|
1990
|
+
if (fieldPath.length === 0) {
|
|
1991
|
+
if (path13.length === 0 && depth < MAX_DEPTH2) iterate(top, { space: "body", path: [] }, lib, c, depth);
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
const field = fieldPath.join(".").replace(/\.\[\]/g, "[]");
|
|
1995
|
+
c.fields.add(field);
|
|
1996
|
+
if (cut < 0) {
|
|
1997
|
+
for (const s of comparedLiterals2(top).strs) {
|
|
1998
|
+
const set = c.enums.get(field) ?? /* @__PURE__ */ new Set();
|
|
1999
|
+
set.add(s);
|
|
2000
|
+
c.enums.set(field, set);
|
|
2001
|
+
}
|
|
2002
|
+
if (depth < MAX_DEPTH2) {
|
|
2003
|
+
const parent = top.parent;
|
|
2004
|
+
if (parent?.type === "assignment" && parent.childForFieldName("right")?.id === top.id) {
|
|
2005
|
+
const left = parent.childForFieldName("left");
|
|
2006
|
+
if (left?.type === "identifier") bindName2(left.text, parent, { space: "body", path: fieldPath }, lib, c, depth + 1);
|
|
2007
|
+
}
|
|
2008
|
+
iterate(top, { space: "body", path: fieldPath }, lib, c, depth);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
function iterate(top, loc, lib, c, depth) {
|
|
2013
|
+
const p = top.parent;
|
|
2014
|
+
const items = { space: "body", path: [...loc.path, "[]"] };
|
|
2015
|
+
if (p?.type === "for_statement" && p.childForFieldName("right")?.id === top.id) {
|
|
2016
|
+
const left = p.childForFieldName("left");
|
|
2017
|
+
if (left?.type === "identifier") bindName2(left.text, left, items, lib, c, depth + 1);
|
|
2018
|
+
} else if (p?.type === "for_in_clause" && p.childForFieldName("right")?.id === top.id) {
|
|
2019
|
+
const left = p.childForFieldName("left");
|
|
2020
|
+
const comp = p.parent;
|
|
2021
|
+
if (left?.type === "identifier" && comp) {
|
|
2022
|
+
for (const id of comp.descendantsOfType("identifier")) {
|
|
2023
|
+
if (id && id.text === left.text && id.id !== left.id) use2(id, items, lib, c, depth + 1);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
function comparedLiterals2(node) {
|
|
2029
|
+
const nums = [];
|
|
2030
|
+
const strs = [];
|
|
2031
|
+
let cur = node;
|
|
2032
|
+
while (cur.parent?.type === "parenthesized_expression") cur = cur.parent;
|
|
2033
|
+
const p = cur.parent;
|
|
2034
|
+
if (p?.type !== "comparison_operator") return { nums, strs };
|
|
2035
|
+
const take = (n) => {
|
|
2036
|
+
if (!n) return;
|
|
2037
|
+
if (n.type === "integer") nums.push(Number(n.text));
|
|
2038
|
+
else if (n.type === "string") {
|
|
2039
|
+
const v = stringValue(n);
|
|
2040
|
+
if (v !== void 0) strs.push(v);
|
|
2041
|
+
} else if (n.type === "tuple" || n.type === "list" || n.type === "set") n.namedChildren.forEach(take);
|
|
2042
|
+
};
|
|
2043
|
+
for (const operand of p.namedChildren) if (operand && operand.id !== cur.id) take(operand);
|
|
2044
|
+
return { nums, strs };
|
|
2045
|
+
}
|
|
2046
|
+
function chainOf2(node) {
|
|
2047
|
+
const names = [];
|
|
2048
|
+
let cur = node;
|
|
2049
|
+
while (cur.type === "attribute") {
|
|
2050
|
+
names.unshift(cur.childForFieldName("attribute").text);
|
|
2051
|
+
cur = cur.childForFieldName("object");
|
|
2052
|
+
}
|
|
2053
|
+
if (cur.type !== "identifier") return void 0;
|
|
2054
|
+
const root = cur.text;
|
|
2055
|
+
return { root, names, prefix: (n) => [root, ...names.slice(0, n)].join(".") };
|
|
2056
|
+
}
|
|
2057
|
+
function unwrap2(n) {
|
|
2058
|
+
let cur = n;
|
|
2059
|
+
while (cur.type === "await" || cur.type === "parenthesized_expression") cur = cur.namedChildren[0] ?? cur;
|
|
2060
|
+
return cur;
|
|
2061
|
+
}
|
|
2062
|
+
function keyword(args, name) {
|
|
2063
|
+
for (const a of args?.namedChildren ?? []) {
|
|
2064
|
+
if (a?.type === "keyword_argument" && a.childForFieldName("name")?.text === name) return a.childForFieldName("value") ?? void 0;
|
|
2065
|
+
}
|
|
2066
|
+
return void 0;
|
|
2067
|
+
}
|
|
2068
|
+
function stringValue(n) {
|
|
2069
|
+
if (!n || n.type !== "string") return void 0;
|
|
2070
|
+
if (n.namedChildren.some((c) => c?.type === "interpolation")) return void 0;
|
|
2071
|
+
return n.namedChildren.filter((c) => c?.type === "string_content" || c?.type === "escape_sequence").map((c) => c.text).join("");
|
|
2072
|
+
}
|
|
2073
|
+
function evalString2(ctx, node, depth = 0) {
|
|
2074
|
+
if (!node || depth > 8) return void 0;
|
|
2075
|
+
const n = unwrap2(node);
|
|
2076
|
+
if (n.type === "string") {
|
|
2077
|
+
let out = "";
|
|
2078
|
+
for (const part of n.namedChildren) {
|
|
2079
|
+
if (!part) continue;
|
|
2080
|
+
if (part.type === "string_content" || part.type === "escape_sequence") out += part.text;
|
|
2081
|
+
else if (part.type === "interpolation") {
|
|
2082
|
+
const inner = part.childForFieldName("expression") ?? part.namedChildren[0];
|
|
2083
|
+
out += (inner && evalString2(ctx, inner, depth + 1)) ?? placeholder2(inner ?? void 0);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
return out;
|
|
2087
|
+
}
|
|
2088
|
+
if (n.type === "concatenated_string") return n.namedChildren.map((s) => evalString2(ctx, s ?? void 0, depth + 1) ?? "").join("");
|
|
2089
|
+
if (n.type === "binary_operator" && n.childForFieldName("operator")?.text === "+") {
|
|
2090
|
+
const l = evalString2(ctx, n.childForFieldName("left") ?? void 0, depth + 1);
|
|
2091
|
+
const r = evalString2(ctx, n.childForFieldName("right") ?? void 0, depth + 1);
|
|
2092
|
+
if (l === void 0 && r === void 0) return void 0;
|
|
2093
|
+
return (l ?? placeholder2(n.childForFieldName("left") ?? void 0)) + (r ?? placeholder2(n.childForFieldName("right") ?? void 0));
|
|
2094
|
+
}
|
|
2095
|
+
if (n.type === "identifier") {
|
|
2096
|
+
const init = localAssignment(n) ?? ctx.consts.get(n.text);
|
|
2097
|
+
return init && init.id !== node.id ? evalString2(ctx, init, depth + 1) : void 0;
|
|
2098
|
+
}
|
|
2099
|
+
if (n.type === "call") {
|
|
2100
|
+
const fn = n.childForFieldName("function")?.text;
|
|
2101
|
+
const args = (n.childForFieldName("arguments")?.namedChildren ?? []).filter(Boolean);
|
|
2102
|
+
if ((fn === "os.getenv" || fn === "getenv" || fn === "os.environ.get") && args[1]) return evalString2(ctx, args[1], depth + 1);
|
|
2103
|
+
}
|
|
2104
|
+
return void 0;
|
|
2105
|
+
}
|
|
2106
|
+
function localAssignment(use3) {
|
|
2107
|
+
let scope = use3.parent;
|
|
2108
|
+
while (scope && scope.type !== "function_definition" && scope.type !== "module") scope = scope.parent;
|
|
2109
|
+
if (!scope || scope.type === "module") return void 0;
|
|
2110
|
+
let last;
|
|
2111
|
+
for (const assign of scope.descendantsOfType("assignment")) {
|
|
2112
|
+
if (!assign || assign.endIndex > use3.startIndex) continue;
|
|
2113
|
+
const left = assign.childForFieldName("left");
|
|
2114
|
+
if (left?.type !== "identifier" || left.text !== use3.text) continue;
|
|
2115
|
+
let owner = assign.parent;
|
|
2116
|
+
while (owner && owner.type !== "function_definition") owner = owner.parent;
|
|
2117
|
+
if (owner?.id !== scope.id) continue;
|
|
2118
|
+
last = assign.childForFieldName("right") ?? void 0;
|
|
2119
|
+
}
|
|
2120
|
+
return last;
|
|
2121
|
+
}
|
|
2122
|
+
function looseUrls(ctx, root, file, sites, calls) {
|
|
2123
|
+
const covered = new Set(sites.map((s) => `${s.provider} ${s.endpoint}`));
|
|
2124
|
+
const inCall = (n) => calls.some((c) => n.startIndex >= c.startIndex && n.endIndex <= c.endIndex);
|
|
2125
|
+
const candidates = [];
|
|
2126
|
+
for (const node of root.descendantsOfType(["string", "concatenated_string", "binary_operator"])) {
|
|
2127
|
+
if (!node) continue;
|
|
2128
|
+
if (node.type === "binary_operator" && node.childForFieldName("operator")?.text !== "+") continue;
|
|
2129
|
+
const p = node.parent;
|
|
2130
|
+
if (p && (p.type === "concatenated_string" || p.type === "interpolation" || p.type === "expression_statement" || p.type === "binary_operator" && p.childForFieldName("operator")?.text === "+")) continue;
|
|
2131
|
+
if (inCall(node)) continue;
|
|
2132
|
+
const value2 = evalString2(ctx, node);
|
|
2133
|
+
const parts = value2 === void 0 ? void 0 : urlParts(value2);
|
|
2134
|
+
if (!parts?.path || parts.path === "/") continue;
|
|
2135
|
+
const provider = providerOf(parts.host, parts.path);
|
|
2136
|
+
if (provider) candidates.push({ node, host: parts.host, path: parts.path, query: parts.query, provider });
|
|
2137
|
+
}
|
|
2138
|
+
const out = [];
|
|
2139
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2140
|
+
for (const c of candidates) {
|
|
2141
|
+
const key = `${c.provider} ${c.path}`;
|
|
2142
|
+
if (covered.has(key) || seen.has(key)) continue;
|
|
2143
|
+
if (candidates.some((o) => o.provider === c.provider && o.path !== c.path && o.path.startsWith(`${c.path}/`))) continue;
|
|
2144
|
+
if (sites.some((s) => s.provider === c.provider && s.endpoint?.startsWith(`${c.path}/`))) continue;
|
|
2145
|
+
seen.add(key);
|
|
2146
|
+
const surface = emptySurface2();
|
|
2147
|
+
surface.query = c.query;
|
|
2148
|
+
out.push({ file, line: c.node.startPosition.row + 1, column: c.node.startPosition.column + 1, provider: c.provider, kind: "http", client: "url", method: null, host: c.host, endpoint: c.path, resolved: true, surface });
|
|
2149
|
+
}
|
|
2150
|
+
return out;
|
|
2151
|
+
}
|
|
2152
|
+
function placeholder2(n) {
|
|
2153
|
+
if (!n) return "{}";
|
|
2154
|
+
const e = unwrap2(n);
|
|
2155
|
+
if (e.type === "identifier") return `{${e.text}}`;
|
|
2156
|
+
if (e.type === "attribute") return `{${e.childForFieldName("attribute")?.text ?? ""}}`;
|
|
2157
|
+
const args = e.type === "call" ? (e.childForFieldName("arguments")?.namedChildren ?? []).filter(Boolean) : [];
|
|
2158
|
+
if (args.length === 1) return placeholder2(args[0]);
|
|
2159
|
+
return "{}";
|
|
2160
|
+
}
|
|
2161
|
+
function dictEntries(ctx, node, depth = 0) {
|
|
2162
|
+
if (!node || depth > 3) return [];
|
|
2163
|
+
const n = unwrap2(node);
|
|
2164
|
+
if (n.type === "identifier") {
|
|
2165
|
+
const init = ctx.consts.get(n.text);
|
|
2166
|
+
return init ? dictEntries(ctx, init, depth + 1) : [];
|
|
2167
|
+
}
|
|
2168
|
+
if (n.type === "call" && n.childForFieldName("function")?.text === "dict") {
|
|
2169
|
+
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) }));
|
|
2170
|
+
}
|
|
2171
|
+
if (n.type !== "dictionary") return [];
|
|
2172
|
+
const out = [];
|
|
2173
|
+
for (const pair of n.namedChildren) {
|
|
2174
|
+
if (pair?.type !== "pair") continue;
|
|
2175
|
+
const key = stringValue(pair.childForFieldName("key") ?? void 0);
|
|
2176
|
+
if (key !== void 0) out.push({ name: key, value: literal(ctx, pair.childForFieldName("value") ?? void 0) });
|
|
2177
|
+
}
|
|
2178
|
+
return out;
|
|
2179
|
+
}
|
|
2180
|
+
function literal(ctx, n) {
|
|
2181
|
+
if (!n) return null;
|
|
2182
|
+
if (n.type === "integer" || n.type === "float") return n.text;
|
|
2183
|
+
if (n.type === "true" || n.type === "false") return n.text.toLowerCase();
|
|
2184
|
+
return evalString2(ctx, n) ?? null;
|
|
2185
|
+
}
|
|
2186
|
+
function joinUrl2(base, url) {
|
|
2187
|
+
if (url === void 0) return base;
|
|
2188
|
+
if (!base || /^[a-z]+:\/\//i.test(url)) return url;
|
|
2189
|
+
return base.replace(/\/+$/, "") + "/" + url.replace(/^\/+/, "");
|
|
2190
|
+
}
|
|
2191
|
+
function emptySurface2() {
|
|
2192
|
+
return { fields: [], statusCodes: [], checksOk: false, enums: [], headers: [], query: [], responseHeaders: [] };
|
|
2193
|
+
}
|
|
1369
2194
|
|
|
1370
2195
|
// packages/extractor/src/index.ts
|
|
1371
2196
|
var SCRIPT_KINDS = {
|
|
@@ -1380,31 +2205,50 @@ var SCRIPT_KINDS = {
|
|
|
1380
2205
|
};
|
|
1381
2206
|
function extractCallSites(rootDir) {
|
|
1382
2207
|
const root = path4.resolve(rootDir);
|
|
1383
|
-
const { sources, manifests } = listRepoFiles(root);
|
|
2208
|
+
const { sources, manifests, python, pythonManifests } = listRepoFiles(root);
|
|
1384
2209
|
const project = new Project({
|
|
1385
2210
|
useInMemoryFileSystem: true,
|
|
1386
2211
|
skipFileDependencyResolution: true,
|
|
1387
2212
|
compilerOptions: { allowJs: true }
|
|
1388
2213
|
});
|
|
1389
2214
|
const found = [];
|
|
2215
|
+
const loose = [];
|
|
1390
2216
|
for (const abs of sources) {
|
|
1391
2217
|
const rel = toPosix(path4.relative(root, abs));
|
|
1392
2218
|
const sf = project.createSourceFile(`/${rel}`, readFileSync3(abs, "utf8"), {
|
|
1393
2219
|
overwrite: true,
|
|
1394
2220
|
scriptKind: SCRIPT_KINDS[path4.extname(abs)] ?? ScriptKind.TS
|
|
1395
2221
|
});
|
|
1396
|
-
|
|
2222
|
+
const inFile = extractFromFile(sf, rel);
|
|
2223
|
+
for (const f of inFile) {
|
|
1397
2224
|
addResponseSurface(f);
|
|
1398
2225
|
found.push(f);
|
|
1399
2226
|
}
|
|
2227
|
+
loose.push(...looseUrlSites(sf, rel, inFile));
|
|
1400
2228
|
}
|
|
1401
2229
|
found.sort((a, b) => a.site.file.localeCompare(b.site.file) || a.site.line - b.site.line || a.site.column - b.site.column);
|
|
2230
|
+
const skipped = [];
|
|
2231
|
+
const pythonSites = [];
|
|
2232
|
+
for (const abs of python) {
|
|
2233
|
+
const rel = toPosix(path4.relative(root, abs));
|
|
2234
|
+
if (!pythonParserLoaded()) {
|
|
2235
|
+
skipped.push({ file: rel, reason: "the Python parser was not loaded" });
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
try {
|
|
2239
|
+
pythonSites.push(...extractPython(readFileSync3(abs, "utf8"), rel));
|
|
2240
|
+
} catch (err) {
|
|
2241
|
+
skipped.push({ file: rel, reason: err instanceof Error ? err.message : String(err) });
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
const callSites = [...found.map((f) => f.site), ...loose, ...pythonSites].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
|
|
1402
2245
|
return {
|
|
1403
2246
|
root,
|
|
1404
|
-
filesScanned: sources.length,
|
|
1405
|
-
callSites
|
|
1406
|
-
declared: declaredProviders(root, manifests),
|
|
1407
|
-
found
|
|
2247
|
+
filesScanned: sources.length + python.length - skipped.length,
|
|
2248
|
+
callSites,
|
|
2249
|
+
declared: [...declaredProviders(root, manifests), ...declaredPythonProviders(root, pythonManifests)],
|
|
2250
|
+
found,
|
|
2251
|
+
skipped
|
|
1408
2252
|
};
|
|
1409
2253
|
}
|
|
1410
2254
|
|
|
@@ -1520,9 +2364,9 @@ function inferShape(res, opts) {
|
|
|
1520
2364
|
else if (!VOLATILE_HEADERS.has(name) && !name.startsWith("x-ratelimit") && !name.startsWith("ratelimit")) headerNames.push(name);
|
|
1521
2365
|
}
|
|
1522
2366
|
const text = res.body ?? "";
|
|
1523
|
-
let
|
|
2367
|
+
let body3;
|
|
1524
2368
|
if (text.trim() === "") {
|
|
1525
|
-
|
|
2369
|
+
body3 = { kind: "empty", fields: [] };
|
|
1526
2370
|
} else {
|
|
1527
2371
|
let parsed;
|
|
1528
2372
|
let isJson = true;
|
|
@@ -1534,16 +2378,16 @@ function inferShape(res, opts) {
|
|
|
1534
2378
|
if (isJson) {
|
|
1535
2379
|
const acc = /* @__PURE__ */ new Map();
|
|
1536
2380
|
walk(parsed, "", acc);
|
|
1537
|
-
|
|
2381
|
+
body3 = { kind: "json", rootType: typeOf(parsed), fields: finish(acc, opts.salt) };
|
|
1538
2382
|
} else {
|
|
1539
|
-
|
|
2383
|
+
body3 = { kind: "text", fields: [] };
|
|
1540
2384
|
}
|
|
1541
2385
|
}
|
|
1542
2386
|
return {
|
|
1543
2387
|
status: res.status,
|
|
1544
2388
|
headers: Object.fromEntries(Object.entries(headers).sort(([a], [b]) => cmp3(a, b))),
|
|
1545
2389
|
headerNames: [...new Set(headerNames)].sort(),
|
|
1546
|
-
body:
|
|
2390
|
+
body: body3
|
|
1547
2391
|
};
|
|
1548
2392
|
}
|
|
1549
2393
|
function walk(value2, path13, acc) {
|
|
@@ -1939,8 +2783,181 @@ function replay(recording, store, secret) {
|
|
|
1939
2783
|
return { recording, observations, after, before, ...before ? { diff: diffShapes(before.shape, after.shape) } : {} };
|
|
1940
2784
|
}
|
|
1941
2785
|
|
|
1942
|
-
// packages/repair/src/recipes/
|
|
2786
|
+
// packages/repair/src/recipes/apply-fix.ts
|
|
1943
2787
|
import { Node as Node3, SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
2788
|
+
var applyFix = {
|
|
2789
|
+
id: "apply-fix",
|
|
2790
|
+
changeClass: "version-sunset",
|
|
2791
|
+
description: "Apply the advisory's fix: the new path, query value or header, and the renamed fields and enum values",
|
|
2792
|
+
edits({ sourceFile, call, advisory }) {
|
|
2793
|
+
const fix2 = advisory.fix;
|
|
2794
|
+
if (!fix2) return [];
|
|
2795
|
+
const where = (n) => `${sourceFile.getFilePath().slice(1)}:${n.getStartLineNumber()}`;
|
|
2796
|
+
const edits = [...requestEdits(call, sourceFile, fix2, where), ...headerEdits(call, fix2, where)];
|
|
2797
|
+
const scope = enclosingFunction(call) ?? sourceFile;
|
|
2798
|
+
edits.push(...fieldEdits(scope, fix2, where, edits), ...enumEdits(scope, fix2, where));
|
|
2799
|
+
return edits;
|
|
2800
|
+
}
|
|
2801
|
+
};
|
|
2802
|
+
var TEXT_KINDS = [SyntaxKind3.StringLiteral, SyntaxKind3.NoSubstitutionTemplateLiteral, SyntaxKind3.TemplateHead, SyntaxKind3.TemplateMiddle, SyntaxKind3.TemplateTail];
|
|
2803
|
+
function requestEdits(call, sf, fix2, where) {
|
|
2804
|
+
if (!fix2.path && !fix2.query) return [];
|
|
2805
|
+
const edits = [];
|
|
2806
|
+
for (const node of urlTextNodes(call, sf)) {
|
|
2807
|
+
const before = node.getText();
|
|
2808
|
+
let after = before;
|
|
2809
|
+
if (fix2.path) {
|
|
2810
|
+
after = after.split(fix2.path.from).join(fix2.path.to);
|
|
2811
|
+
const from = fix2.path.from.replace(/\/$/, "");
|
|
2812
|
+
const to = fix2.path.to.replace(/\/$/, "");
|
|
2813
|
+
if (from !== fix2.path.from) after = after.replace(new RegExp(`${escapeRe(from)}(?=["'\`]|\\$\\{)`, "g"), to);
|
|
2814
|
+
}
|
|
2815
|
+
if (fix2.query) {
|
|
2816
|
+
const re = new RegExp(`([?&])${escapeRe(fix2.query.name)}=([^&#\`'"$]+)`, "g");
|
|
2817
|
+
after = after.replace(re, (_m, sep) => `${sep}${fix2.query.name}=${fix2.query.value}`);
|
|
2818
|
+
}
|
|
2819
|
+
if (after !== before) edits.push({ start: node.getStart(), end: node.getEnd(), text: after, note: `${where(node)} \`${before}\` \u2192 \`${after}\`` });
|
|
2820
|
+
}
|
|
2821
|
+
if (fix2.query) {
|
|
2822
|
+
for (const prop of [...objectProperties(call, "params"), ...objectProperties(call, "searchParams")]) {
|
|
2823
|
+
if (propName2(prop) !== fix2.query.name || !Node3.isPropertyAssignment(prop)) continue;
|
|
2824
|
+
const init = prop.getInitializer();
|
|
2825
|
+
if (!init || !(Node3.isNumericLiteral(init) || Node3.isStringLiteral(init))) continue;
|
|
2826
|
+
const text = /^\d+(\.\d+)?$/.test(fix2.query.value) && Node3.isNumericLiteral(init) ? fix2.query.value : JSON.stringify(fix2.query.value);
|
|
2827
|
+
if (init.getText() !== text) edits.push({ start: init.getStart(), end: init.getEnd(), text, note: `${where(init)} ${fix2.query.name}: \`${init.getText()}\` \u2192 \`${text}\`` });
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
return edits;
|
|
2831
|
+
}
|
|
2832
|
+
function headerEdits(call, fix2, where) {
|
|
2833
|
+
if (!fix2.header) return [];
|
|
2834
|
+
const edits = [];
|
|
2835
|
+
const text = JSON.stringify(fix2.header.value);
|
|
2836
|
+
for (const prop of objectProperties(call, "headers")) {
|
|
2837
|
+
if (propName2(prop)?.toLowerCase() !== fix2.header.name.toLowerCase() || !Node3.isPropertyAssignment(prop)) continue;
|
|
2838
|
+
const init = prop.getInitializer();
|
|
2839
|
+
if (!init || !(Node3.isStringLiteral(init) || Node3.isNoSubstitutionTemplateLiteral(init)) || init.getLiteralText() === fix2.header.value) continue;
|
|
2840
|
+
edits.push({ start: init.getStart(), end: init.getEnd(), text, note: `${where(init)} ${fix2.header.name}: \`${init.getText()}\` \u2192 \`${text}\`` });
|
|
2841
|
+
}
|
|
2842
|
+
return edits;
|
|
2843
|
+
}
|
|
2844
|
+
function fieldEdits(scope, fix2, where, taken) {
|
|
2845
|
+
const edits = [];
|
|
2846
|
+
for (const { from, to } of fix2.fields ?? []) {
|
|
2847
|
+
const oldName = leaf(from);
|
|
2848
|
+
const newName = leaf(to);
|
|
2849
|
+
if (oldName === newName) continue;
|
|
2850
|
+
for (const access of scope.getDescendantsOfKind(SyntaxKind3.PropertyAccessExpression)) {
|
|
2851
|
+
if (access.getName() !== oldName || isWrite(access) || overlaps(access, [...taken, ...edits])) continue;
|
|
2852
|
+
const target = access.getExpression().getText();
|
|
2853
|
+
const dot = access.hasQuestionDotToken() ? "?." : ".";
|
|
2854
|
+
const text = `(${target}${dot}${newName} ?? ${target}${dot}${oldName})`;
|
|
2855
|
+
edits.push({ start: access.getStart(), end: access.getEnd(), text, note: `${where(access)} \`${access.getText()}\` \u2192 \`${text}\`` });
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
return edits;
|
|
2859
|
+
}
|
|
2860
|
+
function enumEdits(scope, fix2, where) {
|
|
2861
|
+
const edits = [];
|
|
2862
|
+
for (const { field, values } of fix2.enums ?? []) {
|
|
2863
|
+
const name = leaf(field);
|
|
2864
|
+
const renamed = new Map(values.map((v) => [v.from, v.to]));
|
|
2865
|
+
const isField = (n) => Node3.isPropertyAccessExpression(n) && n.getName() === name || Node3.isIdentifier(n) && n.getText() === name;
|
|
2866
|
+
for (const bin of scope.getDescendantsOfKind(SyntaxKind3.BinaryExpression)) {
|
|
2867
|
+
const op = bin.getOperatorToken().getKind();
|
|
2868
|
+
const negated = op === SyntaxKind3.ExclamationEqualsEqualsToken || op === SyntaxKind3.ExclamationEqualsToken;
|
|
2869
|
+
if (!negated && op !== SyntaxKind3.EqualsEqualsEqualsToken && op !== SyntaxKind3.EqualsEqualsToken) continue;
|
|
2870
|
+
const [lit, other] = Node3.isStringLiteral(bin.getRight()) ? [bin.getRight(), bin.getLeft()] : [bin.getLeft(), bin.getRight()];
|
|
2871
|
+
if (!Node3.isStringLiteral(lit) || !isField(other)) continue;
|
|
2872
|
+
const to = renamed.get(lit.getLiteralText());
|
|
2873
|
+
if (to === void 0) continue;
|
|
2874
|
+
const text = `${negated ? "!" : ""}${JSON.stringify([lit.getLiteralText(), to])}.includes(${other.getText()})`;
|
|
2875
|
+
edits.push({ start: bin.getStart(), end: bin.getEnd(), text, note: `${where(bin)} \`${bin.getText()}\` \u2192 \`${text}\`` });
|
|
2876
|
+
}
|
|
2877
|
+
for (const sw of scope.getDescendantsOfKind(SyntaxKind3.SwitchStatement)) {
|
|
2878
|
+
if (!isField(sw.getExpression())) continue;
|
|
2879
|
+
const cases = sw.getClauses().filter(Node3.isCaseClause);
|
|
2880
|
+
const present = new Set(cases.map((c) => c.getExpression()).filter(Node3.isStringLiteral).map((l) => l.getLiteralText()));
|
|
2881
|
+
for (const c of cases) {
|
|
2882
|
+
const e = c.getExpression();
|
|
2883
|
+
const to = Node3.isStringLiteral(e) ? renamed.get(e.getLiteralText()) : void 0;
|
|
2884
|
+
if (to === void 0 || present.has(to)) continue;
|
|
2885
|
+
present.add(to);
|
|
2886
|
+
const indent = " ".repeat(c.getSourceFile().getLineAndColumnAtPos(c.getStart()).column - 1);
|
|
2887
|
+
edits.push({ start: c.getStart(), end: c.getStart(), text: `case ${JSON.stringify(to)}:
|
|
2888
|
+
${indent}`, note: `${where(c)} add case ${JSON.stringify(to)}` });
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
for (const inc of scope.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
|
|
2892
|
+
const callee = inc.getExpression();
|
|
2893
|
+
if (!Node3.isPropertyAccessExpression(callee) || callee.getName() !== "includes" || !isField(inc.getArguments()[0] ?? inc)) continue;
|
|
2894
|
+
const arr = callee.getExpression();
|
|
2895
|
+
if (!Node3.isArrayLiteralExpression(arr)) continue;
|
|
2896
|
+
const items = arr.getElements().filter(Node3.isStringLiteral).map((l) => l.getLiteralText());
|
|
2897
|
+
const add = items.map((v) => renamed.get(v)).filter((v) => v !== void 0 && !items.includes(v));
|
|
2898
|
+
if (!add.length) continue;
|
|
2899
|
+
const text = JSON.stringify([...items, ...new Set(add)]);
|
|
2900
|
+
edits.push({ start: arr.getStart(), end: arr.getEnd(), text, note: `${where(arr)} \`${arr.getText()}\` \u2192 \`${text}\`` });
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
return edits;
|
|
2904
|
+
}
|
|
2905
|
+
function urlTextNodes(call, sf) {
|
|
2906
|
+
const out = /* @__PURE__ */ new Set();
|
|
2907
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2908
|
+
const visit = (n, depth) => {
|
|
2909
|
+
if (!n || depth > 4 || seen.has(n)) return;
|
|
2910
|
+
seen.add(n);
|
|
2911
|
+
if (TEXT_KINDS.includes(n.getKind())) out.add(n);
|
|
2912
|
+
if (Node3.isIdentifier(n)) {
|
|
2913
|
+
const decl = sf.getVariableDeclaration(n.getText());
|
|
2914
|
+
if (decl?.getVariableStatement()?.getDeclarationKind() === "const") visit(decl.getInitializer(), depth + 1);
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
n.forEachChild((c) => visit(c, depth));
|
|
2918
|
+
};
|
|
2919
|
+
const [first] = call.getArguments();
|
|
2920
|
+
if (first && Node3.isObjectLiteralExpression(first)) {
|
|
2921
|
+
const url = first.getProperty("url");
|
|
2922
|
+
if (url && Node3.isPropertyAssignment(url)) visit(url.getInitializer(), 0);
|
|
2923
|
+
} else visit(first, 0);
|
|
2924
|
+
for (const prop of sf.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) if (propName2(prop) === "baseURL") visit(prop.getInitializer(), 0);
|
|
2925
|
+
return [...out];
|
|
2926
|
+
}
|
|
2927
|
+
function objectProperties(call, name) {
|
|
2928
|
+
const out = [];
|
|
2929
|
+
for (const arg of call.getArguments()) {
|
|
2930
|
+
if (!Node3.isObjectLiteralExpression(arg)) continue;
|
|
2931
|
+
const prop = arg.getProperties().find((p) => propName2(p) === name);
|
|
2932
|
+
const value2 = prop && Node3.isPropertyAssignment(prop) ? prop.getInitializer() : void 0;
|
|
2933
|
+
if (value2 && Node3.isObjectLiteralExpression(value2)) out.push(...value2.getProperties());
|
|
2934
|
+
}
|
|
2935
|
+
return out;
|
|
2936
|
+
}
|
|
2937
|
+
function propName2(p) {
|
|
2938
|
+
if (!(Node3.isPropertyAssignment(p) || Node3.isShorthandPropertyAssignment(p))) return void 0;
|
|
2939
|
+
const n = p.getNameNode();
|
|
2940
|
+
return Node3.isStringLiteral(n) || Node3.isNoSubstitutionTemplateLiteral(n) ? n.getLiteralText() : n.getText();
|
|
2941
|
+
}
|
|
2942
|
+
function enclosingFunction(call) {
|
|
2943
|
+
return call.getFirstAncestor((n) => Node3.isFunctionDeclaration(n) || Node3.isArrowFunction(n) || Node3.isFunctionExpression(n) || Node3.isMethodDeclaration(n));
|
|
2944
|
+
}
|
|
2945
|
+
function isWrite(access) {
|
|
2946
|
+
const p = access.getParent();
|
|
2947
|
+
return Node3.isBinaryExpression(p) && p.getLeft() === access && p.getOperatorToken().getKind() === SyntaxKind3.EqualsToken;
|
|
2948
|
+
}
|
|
2949
|
+
function overlaps(n, edits) {
|
|
2950
|
+
return edits.some((e) => n.getStart() < e.end && e.start < n.getEnd());
|
|
2951
|
+
}
|
|
2952
|
+
function leaf(path13) {
|
|
2953
|
+
return path13.split(".").pop().replace(/\[\]/g, "");
|
|
2954
|
+
}
|
|
2955
|
+
function escapeRe(s) {
|
|
2956
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
// packages/repair/src/recipes/status-code-accept-set.ts
|
|
2960
|
+
import { Node as Node4, SyntaxKind as SyntaxKind4 } from "ts-morph";
|
|
1944
2961
|
var STATUS_PROPS = /* @__PURE__ */ new Set(["status", "statusCode"]);
|
|
1945
2962
|
var statusCodeAcceptSet = {
|
|
1946
2963
|
id: "status-code-accept-set",
|
|
@@ -1955,23 +2972,23 @@ var statusCodeAcceptSet = {
|
|
|
1955
2972
|
const where = (n) => `${sourceFile.getFilePath().slice(1)}:${n.getStartLineNumber()}`;
|
|
1956
2973
|
for (const status of statusExpressions(call)) {
|
|
1957
2974
|
let cur = status;
|
|
1958
|
-
while (
|
|
2975
|
+
while (Node4.isParenthesizedExpression(cur.getParent())) cur = cur.getParent();
|
|
1959
2976
|
const parent = cur.getParent();
|
|
1960
|
-
if (
|
|
2977
|
+
if (Node4.isBinaryExpression(parent)) {
|
|
1961
2978
|
const op = parent.getOperatorToken().getKind();
|
|
1962
2979
|
const other = parent.getLeft() === cur ? parent.getRight() : parent.getLeft();
|
|
1963
|
-
if (!
|
|
1964
|
-
const negated = op ===
|
|
1965
|
-
const equal = op ===
|
|
2980
|
+
if (!Node4.isNumericLiteral(other) || !oldCodes.includes(Number(other.getLiteralValue()))) continue;
|
|
2981
|
+
const negated = op === SyntaxKind4.ExclamationEqualsEqualsToken || op === SyntaxKind4.ExclamationEqualsToken;
|
|
2982
|
+
const equal = op === SyntaxKind4.EqualsEqualsEqualsToken || op === SyntaxKind4.EqualsEqualsToken;
|
|
1966
2983
|
if (!negated && !equal) continue;
|
|
1967
2984
|
const text = `${negated ? "!" : ""}${list}.includes(${status.getText()})`;
|
|
1968
2985
|
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(
|
|
2986
|
+
} else if (Node4.isSwitchStatement(parent) && parent.getExpression() === cur) {
|
|
2987
|
+
const cases = parent.getClauses().filter(Node4.isCaseClause);
|
|
2988
|
+
const present = new Set(cases.map((c) => c.getExpression()).filter(Node4.isNumericLiteral).map((l) => Number(l.getLiteralValue())));
|
|
1972
2989
|
const anchor = cases.find((c) => {
|
|
1973
2990
|
const e = c.getExpression();
|
|
1974
|
-
return
|
|
2991
|
+
return Node4.isNumericLiteral(e) && oldCodes.includes(Number(e.getLiteralValue()));
|
|
1975
2992
|
});
|
|
1976
2993
|
const missing = accept.filter((c) => !present.has(c));
|
|
1977
2994
|
if (!anchor || missing.length === 0) continue;
|
|
@@ -1979,11 +2996,11 @@ var statusCodeAcceptSet = {
|
|
|
1979
2996
|
const text = missing.map((c) => `case ${c}:
|
|
1980
2997
|
${indent}`).join("");
|
|
1981
2998
|
edits.push({ start: anchor.getStart(), end: anchor.getStart(), text, note: `${where(anchor)} add ${missing.map((c) => `case ${c}`).join(", ")}` });
|
|
1982
|
-
} else if (
|
|
2999
|
+
} else if (Node4.isCallExpression(parent) && parent.getArguments()[0] === cur) {
|
|
1983
3000
|
const callee = parent.getExpression();
|
|
1984
|
-
const arr =
|
|
1985
|
-
if (!arr || !
|
|
1986
|
-
const values = arr.getElements().filter(
|
|
3001
|
+
const arr = Node4.isPropertyAccessExpression(callee) && callee.getName() === "includes" ? callee.getExpression() : void 0;
|
|
3002
|
+
if (!arr || !Node4.isArrayLiteralExpression(arr)) continue;
|
|
3003
|
+
const values = arr.getElements().filter(Node4.isNumericLiteral).map((l) => Number(l.getLiteralValue()));
|
|
1987
3004
|
if (!values.some((v) => oldCodes.includes(v))) continue;
|
|
1988
3005
|
const merged = [.../* @__PURE__ */ new Set([...values, ...accept])].sort((a, b) => a - b);
|
|
1989
3006
|
if (merged.length === values.length) continue;
|
|
@@ -1999,52 +3016,52 @@ function statusExpressions(call) {
|
|
|
1999
3016
|
let cur = call;
|
|
2000
3017
|
for (; ; ) {
|
|
2001
3018
|
const p = cur.getParent();
|
|
2002
|
-
if (p && (
|
|
3019
|
+
if (p && (Node4.isAwaitExpression(p) || Node4.isParenthesizedExpression(p) || Node4.isAsExpression(p) || Node4.isNonNullExpression(p))) cur = p;
|
|
2003
3020
|
else break;
|
|
2004
3021
|
}
|
|
2005
3022
|
const parent = cur.getParent();
|
|
2006
3023
|
const sf = call.getSourceFile();
|
|
2007
|
-
if (
|
|
2008
|
-
if (
|
|
2009
|
-
const cb =
|
|
2010
|
-
const param = cb && (
|
|
3024
|
+
if (Node4.isPropertyAccessExpression(parent) && parent.getExpression() === cur && STATUS_PROPS.has(parent.getName())) out.push(parent);
|
|
3025
|
+
if (Node4.isPropertyAccessExpression(parent) && parent.getName() === "then") {
|
|
3026
|
+
const cb = Node4.isCallExpression(parent.getParent()) ? parent.getParent().getArguments()[0] : void 0;
|
|
3027
|
+
const param = cb && (Node4.isArrowFunction(cb) || Node4.isFunctionExpression(cb)) ? cb.getParameters()[0] : void 0;
|
|
2011
3028
|
if (param) out.push(...fromBinding(param.getNameNode(), param, sf));
|
|
2012
3029
|
}
|
|
2013
|
-
if (
|
|
3030
|
+
if (Node4.isVariableDeclaration(parent) && parent.getInitializer() === cur) out.push(...fromBinding(parent.getNameNode(), parent, sf));
|
|
2014
3031
|
return out;
|
|
2015
3032
|
}
|
|
2016
3033
|
function fromBinding(name, decl, sf) {
|
|
2017
3034
|
const out = [];
|
|
2018
|
-
if (
|
|
3035
|
+
if (Node4.isIdentifier(name)) {
|
|
2019
3036
|
for (const ref of references(name.getText(), decl, sf)) {
|
|
2020
3037
|
const p = ref.getParent();
|
|
2021
|
-
if (
|
|
3038
|
+
if (Node4.isPropertyAccessExpression(p) && p.getExpression() === ref && STATUS_PROPS.has(p.getName())) out.push(p);
|
|
2022
3039
|
}
|
|
2023
|
-
} else if (
|
|
3040
|
+
} else if (Node4.isObjectBindingPattern(name)) {
|
|
2024
3041
|
for (const el of name.getElements()) {
|
|
2025
3042
|
const prop = el.getPropertyNameNode()?.getText() ?? el.getName();
|
|
2026
|
-
if (STATUS_PROPS.has(prop) &&
|
|
3043
|
+
if (STATUS_PROPS.has(prop) && Node4.isIdentifier(el.getNameNode())) out.push(...references(el.getName(), decl, sf));
|
|
2027
3044
|
}
|
|
2028
3045
|
}
|
|
2029
3046
|
return out;
|
|
2030
3047
|
}
|
|
2031
3048
|
function references(name, decl, sf) {
|
|
2032
3049
|
const scope = decl.getFirstAncestor(
|
|
2033
|
-
(n) =>
|
|
3050
|
+
(n) => Node4.isBlock(n) || Node4.isArrowFunction(n) || Node4.isFunctionExpression(n) || Node4.isFunctionDeclaration(n) || Node4.isMethodDeclaration(n) || Node4.isCaseClause(n)
|
|
2034
3051
|
) ?? sf;
|
|
2035
|
-
return scope.getDescendantsOfKind(
|
|
3052
|
+
return scope.getDescendantsOfKind(SyntaxKind4.Identifier).filter((id) => {
|
|
2036
3053
|
if (id.getText() !== name || id.getStart() < decl.getEnd()) return false;
|
|
2037
3054
|
const p = id.getParent();
|
|
2038
|
-
if (
|
|
2039
|
-
if (
|
|
2040
|
-
return !(
|
|
3055
|
+
if (Node4.isPropertyAccessExpression(p) && p.getNameNode() === id) return false;
|
|
3056
|
+
if (Node4.isPropertyAssignment(p) && p.getNameNode() === id) return false;
|
|
3057
|
+
return !(Node4.isBindingElement(p) || Node4.isVariableDeclaration(p) || Node4.isParameterDeclaration(p));
|
|
2041
3058
|
});
|
|
2042
3059
|
}
|
|
2043
3060
|
|
|
2044
3061
|
// packages/repair/src/recipe.ts
|
|
2045
|
-
import { Project as Project2, ScriptKind as ScriptKind2, SyntaxKind as
|
|
3062
|
+
import { Project as Project2, ScriptKind as ScriptKind2, SyntaxKind as SyntaxKind5 } from "ts-morph";
|
|
2046
3063
|
function findCall(sf, site) {
|
|
2047
|
-
return sf.getDescendantsOfKind(
|
|
3064
|
+
return sf.getDescendantsOfKind(SyntaxKind5.CallExpression).filter((c) => {
|
|
2048
3065
|
const pos = sf.getLineAndColumnAtPos(c.getStart());
|
|
2049
3066
|
return pos.line === site.line && pos.column === site.column;
|
|
2050
3067
|
}).sort((a, b) => a.getWidth() - b.getWidth())[0];
|
|
@@ -2073,7 +3090,7 @@ function runRecipe(recipes, advisory, callSite, text) {
|
|
|
2073
3090
|
|
|
2074
3091
|
// packages/repair/src/contract.ts
|
|
2075
3092
|
import path7 from "path";
|
|
2076
|
-
import { Node as
|
|
3093
|
+
import { Node as Node5, Project as Project3, ScriptKind as ScriptKind3, SyntaxKind as SyntaxKind6 } from "ts-morph";
|
|
2077
3094
|
|
|
2078
3095
|
// packages/repair/src/synth.ts
|
|
2079
3096
|
function synthesizeBody(shape) {
|
|
@@ -2132,70 +3149,339 @@ function sampleString(classes) {
|
|
|
2132
3149
|
if (classes.includes("token")) return "shapi_value";
|
|
2133
3150
|
return "shapi text";
|
|
2134
3151
|
}
|
|
3152
|
+
function bodyFromPaths(paths, values = {}) {
|
|
3153
|
+
const all = [.../* @__PURE__ */ new Set([...paths, ...Object.keys(values)])];
|
|
3154
|
+
const isLeaf = (p) => !all.some((o) => o !== p && (o.startsWith(`${p}.`) || o.startsWith(`${p}[]`)));
|
|
3155
|
+
const shape = {
|
|
3156
|
+
status: 200,
|
|
3157
|
+
headers: {},
|
|
3158
|
+
headerNames: [],
|
|
3159
|
+
body: {
|
|
3160
|
+
kind: "json",
|
|
3161
|
+
rootType: all.some((p) => p.startsWith("[]")) ? "array" : "object",
|
|
3162
|
+
fields: all.map((path13) => ({ path: path13, pathHash: path13, types: isLeaf(path13) ? ["string"] : [], classes: [], occurrences: 1, nullRate: 0 }))
|
|
3163
|
+
}
|
|
3164
|
+
};
|
|
3165
|
+
const body3 = synthesizeBody(shape);
|
|
3166
|
+
for (const [path13, value2] of Object.entries(values)) setPath(body3, tokens(path13), value2);
|
|
3167
|
+
return body3;
|
|
3168
|
+
}
|
|
3169
|
+
function setPath(target, path13, value2) {
|
|
3170
|
+
let cur = target;
|
|
3171
|
+
for (let i = 0; i < path13.length; i++) {
|
|
3172
|
+
const t = path13[i];
|
|
3173
|
+
const last = i === path13.length - 1;
|
|
3174
|
+
if (t === "[]") {
|
|
3175
|
+
const arr = cur;
|
|
3176
|
+
if (last) arr[0] = value2;
|
|
3177
|
+
else cur = arr[0];
|
|
3178
|
+
} else {
|
|
3179
|
+
const obj = cur;
|
|
3180
|
+
if (last) obj[t] = value2;
|
|
3181
|
+
else cur = obj[t];
|
|
3182
|
+
}
|
|
3183
|
+
if (cur === void 0 || cur === null) return;
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
2135
3186
|
|
|
2136
3187
|
// packages/repair/src/contract.ts
|
|
2137
3188
|
var CONTRACT_DIR = "test/shapi";
|
|
3189
|
+
var RETIRED_STATUS = 410;
|
|
2138
3190
|
function generateContractTest(input) {
|
|
2139
3191
|
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
|
-
}
|
|
3192
|
+
const fix2 = advisory.fix;
|
|
2143
3193
|
const project = new Project3({ useInMemoryFileSystem: true, skipFileDependencyResolution: true, compilerOptions: { allowJs: true } });
|
|
2144
3194
|
const sf = project.createSourceFile(`/${callSite.file}`, input.sourceText, {
|
|
2145
3195
|
scriptKind: /\.(c|m)?jsx?$/.test(callSite.file) ? ScriptKind3.JS : ScriptKind3.TS
|
|
2146
3196
|
});
|
|
2147
3197
|
const call = findCall(sf, callSite);
|
|
2148
3198
|
if (!call) return { status: "unsupported", reason: `no call at ${callSite.file}:${callSite.line}:${callSite.column}` };
|
|
3199
|
+
const transport = transportFor(callSite, sf);
|
|
3200
|
+
if ("reason" in transport) return { status: "unsupported", reason: transport.reason };
|
|
2149
3201
|
const fn = exportedFunction(call);
|
|
2150
3202
|
if (!fn) return { status: "unsupported", reason: "the call site is not inside an exported function" };
|
|
2151
|
-
|
|
3203
|
+
const file = input.file ?? `${CONTRACT_DIR}/${advisory.id}.test.ts`;
|
|
3204
|
+
const importPath = toImport(path7.posix.relative(path7.posix.dirname(file), callSite.file));
|
|
3205
|
+
const parts = {
|
|
3206
|
+
header: `// Generated by SHAPI for advisory ${advisory.id}: ${advisory.title}
|
|
3207
|
+
// Call site: ${callSite.file}:${callSite.line} (${callSite.method} ${callSite.endpoint}, via ${callSite.client})`,
|
|
3208
|
+
fn: fn.name,
|
|
3209
|
+
importPath,
|
|
3210
|
+
args: fn.params.map((p) => argument(p, sf)).join(", "),
|
|
3211
|
+
advisory,
|
|
3212
|
+
transport
|
|
3213
|
+
};
|
|
3214
|
+
if (fix2 && (fix2.path || fix2.query || fix2.header)) {
|
|
3215
|
+
if (callSite.kind === "sdk") return { status: "unsupported", reason: `the fix changes the request, which ${callSite.client} builds itself` };
|
|
3216
|
+
return { status: "generated", file, content: requestTest({ ...parts, fix: fix2, body: renamePair(callSite, fix2).after }), functionName: fn.name };
|
|
3217
|
+
}
|
|
3218
|
+
if (fix2 && (fix2.fields || fix2.enums)) {
|
|
3219
|
+
return { status: "generated", file, content: sameResultTest({ ...parts, ...renamePair(callSite, fix2) }), functionName: fn.name };
|
|
3220
|
+
}
|
|
3221
|
+
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
3222
|
try {
|
|
2153
|
-
|
|
3223
|
+
return { status: "generated", file, content: recordedTest({ ...parts, before, after }), functionName: fn.name };
|
|
2154
3224
|
} catch (err) {
|
|
2155
3225
|
return { status: "unsupported", reason: err instanceof Error ? err.message : String(err) };
|
|
2156
3226
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
3227
|
+
}
|
|
3228
|
+
function transportFor(site, sf) {
|
|
3229
|
+
if (site.kind === "http") {
|
|
3230
|
+
if (site.client === "fetch" || site.client === "ky") return { mock: "", stubFetch: true, name: "fetch" };
|
|
3231
|
+
if (site.client === "node-fetch") {
|
|
3232
|
+
return { mock: `vi.mock("node-fetch", () => ({ default: (input: RequestInfo | URL, init?: RequestInit) => globalThis.fetch(input, init) }));
|
|
3233
|
+
`, stubFetch: true, name: "node-fetch" };
|
|
3234
|
+
}
|
|
3235
|
+
if (site.client === "axios") return { mock: AXIOS_FAKE, stubFetch: false, name: "axios" };
|
|
3236
|
+
if (site.client === "got") return { mock: GOT_FAKE, stubFetch: false, name: "got" };
|
|
3237
|
+
return { reason: `contract tests support fetch, node-fetch, ky, axios, got and SDK call sites; this one uses ${site.client}` };
|
|
3238
|
+
}
|
|
3239
|
+
const decl = sf.getImportDeclarations().find((d) => d.getModuleSpecifierValue() === site.client);
|
|
3240
|
+
if (!decl) return { reason: `${site.file} does not import ${site.client} with an import statement` };
|
|
3241
|
+
const names = decl.getNamedImports().map((n) => n.getName());
|
|
3242
|
+
const exports = [`default: client`, ...names.map((n) => `${JSON.stringify(n)}: client`)].join(", ");
|
|
3243
|
+
const url = `https://${site.host ?? "sdk.invalid"}${site.endpoint ?? "/"}`;
|
|
3244
|
+
const envelope = site.provider === "github" ? `{ status: res.status, data: res.body, headers: res.headers }` : `res.body`;
|
|
3245
|
+
return {
|
|
3246
|
+
mock: `vi.mock(${JSON.stringify(site.client)}, () => {
|
|
3247
|
+
const call = async () => {
|
|
3248
|
+
const res = shapi.respond({ method: ${JSON.stringify(site.method ?? "GET")}, url: ${JSON.stringify(url)}, headers: {} });
|
|
3249
|
+
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 } });
|
|
3250
|
+
return ${envelope};
|
|
3251
|
+
};
|
|
3252
|
+
const client: any = new Proxy(function () {}, {
|
|
3253
|
+
get: (_t, prop) => (prop === "then" || typeof prop === "symbol" ? undefined : client),
|
|
3254
|
+
apply: () => call(),
|
|
3255
|
+
construct: () => client,
|
|
3256
|
+
});
|
|
3257
|
+
return { ${exports} };
|
|
3258
|
+
});
|
|
3259
|
+
`,
|
|
3260
|
+
stubFetch: false,
|
|
3261
|
+
name: `a fake ${site.client} client`
|
|
3262
|
+
};
|
|
3263
|
+
}
|
|
3264
|
+
function preamble(p) {
|
|
3265
|
+
return `import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3266
|
+
|
|
3267
|
+
type ShapiRequest = { method: string; url: string; headers: Record<string, string> };
|
|
3268
|
+
type ShapiResponse = { status: number; body: unknown; headers: Record<string, string> };
|
|
3269
|
+
const shapi = vi.hoisted(() => ({
|
|
3270
|
+
respond: (_req: { method: string; url: string; headers: Record<string, string> }): { status: number; body: unknown; headers: Record<string, string> } => ({ status: 200, body: null, headers: {} }),
|
|
3271
|
+
lower: (h: unknown): Record<string, string> =>
|
|
3272
|
+
Object.fromEntries(Object.entries((h ?? {}) as Record<string, unknown>).filter(([, v]) => v !== undefined).map(([k, v]) => [k.toLowerCase(), String(v)])),
|
|
3273
|
+
}));
|
|
3274
|
+
${p.transport.mock}
|
|
3275
|
+
import { ${p.fn} } from "${p.importPath}";
|
|
3276
|
+
${p.transport.stubFetch ? `
|
|
3277
|
+
const shapiFetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
3278
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
3279
|
+
const headers = Object.fromEntries(new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)).entries());
|
|
3280
|
+
const res = shapi.respond({ method: (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase(), url, headers });
|
|
3281
|
+
return new Response(res.body === null ? null : JSON.stringify(res.body), { status: res.status, headers: res.headers });
|
|
3282
|
+
});
|
|
3283
|
+
` : ""}
|
|
3284
|
+
afterEach(() => {
|
|
3285
|
+
vi.unstubAllGlobals();
|
|
3286
|
+
});
|
|
2166
3287
|
|
|
3288
|
+
async function run(respond: (req: ShapiRequest) => ShapiResponse): Promise<unknown> {
|
|
3289
|
+
shapi.respond = respond;${p.transport.stubFetch ? `
|
|
3290
|
+
vi.stubGlobal("fetch", shapiFetch);` : ""}
|
|
3291
|
+
return ${p.fn}(${p.args});
|
|
3292
|
+
}
|
|
3293
|
+
`;
|
|
3294
|
+
}
|
|
3295
|
+
var JSON_HEADERS = `{ "content-type": "application/json" }`;
|
|
3296
|
+
function requestTest(p) {
|
|
3297
|
+
const checks = [];
|
|
3298
|
+
if (p.fix.path) checks.push(`url.href.includes(${JSON.stringify(p.fix.path.from)})`);
|
|
3299
|
+
if (p.fix.query) {
|
|
3300
|
+
const name = JSON.stringify(p.fix.query.name);
|
|
3301
|
+
const below = p.advisory.trigger.query?.find((q) => q.name === p.fix.query.name)?.below;
|
|
3302
|
+
checks.push(
|
|
3303
|
+
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})`
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
if (p.fix.header) {
|
|
3307
|
+
const name = JSON.stringify(p.fix.header.name.toLowerCase());
|
|
3308
|
+
checks.push(`(${name} in req.headers && req.headers[${name}] !== ${JSON.stringify(p.fix.header.value)})`);
|
|
3309
|
+
}
|
|
3310
|
+
return `${p.header}
|
|
3311
|
+
// The provider no longer accepts the old request: ${p.transport.name} is replaced by a stub that answers it
|
|
3312
|
+
// with ${RETIRED_STATUS}, and answers anything else with a body holding the fields the code reads. The test
|
|
3313
|
+
// fails until ${p.fn}() sends the new request (${describeFix(p.fix)}) and still works.
|
|
3314
|
+
${preamble(p)}
|
|
3315
|
+
const body = ${JSON.stringify(p.body)};
|
|
3316
|
+
|
|
3317
|
+
function retired(req: ShapiRequest): boolean {
|
|
3318
|
+
const url = new URL(req.url, "https://shapi.invalid");
|
|
3319
|
+
return ${checks.join(" || ")};
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
describe("SHAPI contract: ${p.advisory.id}", () => {
|
|
3323
|
+
it("${p.fn} sends the new request and handles the response", async () => {
|
|
3324
|
+
const sent: boolean[] = [];
|
|
3325
|
+
await run((req) => {
|
|
3326
|
+
const old = retired(req);
|
|
3327
|
+
sent.push(old);
|
|
3328
|
+
return old ? { status: ${RETIRED_STATUS}, body: { error: "retired request (SHAPI contract test)" }, headers: ${JSON_HEADERS} } : { status: 200, body, headers: ${JSON_HEADERS} };
|
|
3329
|
+
});
|
|
3330
|
+
expect(sent.length).toBeGreaterThan(0);
|
|
3331
|
+
expect(sent).not.toContain(true);
|
|
3332
|
+
});
|
|
3333
|
+
});
|
|
3334
|
+
`;
|
|
3335
|
+
}
|
|
3336
|
+
function sameResultTest(p) {
|
|
3337
|
+
return `${p.header}
|
|
3338
|
+
// The provider renamed fields or enum values. This test calls ${p.fn}() once with the old response and
|
|
3339
|
+
// once with the new one (${p.transport.name} replaced by a stub), and expects the same result: it fails
|
|
3340
|
+
// while the code only reads the old names.
|
|
3341
|
+
${preamble(p)}
|
|
2167
3342
|
const responses = [
|
|
2168
|
-
${
|
|
3343
|
+
{ name: "old", body: ${JSON.stringify(p.before)} },
|
|
3344
|
+
{ name: "new", body: ${JSON.stringify(p.after)} },
|
|
2169
3345
|
];
|
|
2170
3346
|
|
|
2171
|
-
describe("SHAPI contract: ${advisory.id}", () => {
|
|
2172
|
-
|
|
2173
|
-
|
|
3347
|
+
describe("SHAPI contract: ${p.advisory.id}", () => {
|
|
3348
|
+
it("${p.fn} returns the same result for the old and the new response", async () => {
|
|
3349
|
+
const results: unknown[] = [];
|
|
3350
|
+
for (const r of responses) results.push(await run(() => ({ status: 200, body: r.body, headers: ${JSON_HEADERS} })));
|
|
3351
|
+
expect(results[1]).toEqual(results[0]);
|
|
2174
3352
|
});
|
|
3353
|
+
});
|
|
3354
|
+
`;
|
|
3355
|
+
}
|
|
3356
|
+
function recordedTest(p) {
|
|
3357
|
+
const responses = [response(`before (${p.before.status})`, p.before), response(`after (${p.after.status})`, p.after)].join(",\n");
|
|
3358
|
+
return `${p.header}
|
|
3359
|
+
// Calls ${p.fn}() with ${p.transport.name} returning the response the code was written for and the one
|
|
3360
|
+
// ${p.advisory.provider} sends now. It fails while the code accepts only the old response.
|
|
3361
|
+
${preamble(p)}
|
|
3362
|
+
const responses = [
|
|
3363
|
+
${responses},
|
|
3364
|
+
];
|
|
2175
3365
|
|
|
3366
|
+
describe("SHAPI contract: ${p.advisory.id}", () => {
|
|
2176
3367
|
for (const r of responses) {
|
|
2177
|
-
it(\`${fn
|
|
2178
|
-
const
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
expect(fetchMock).toHaveBeenCalled();
|
|
3368
|
+
it(\`${p.fn} handles the \${r.name} response\`, async () => {
|
|
3369
|
+
const seen: ShapiRequest[] = [];
|
|
3370
|
+
await run((req) => (seen.push(req), { status: r.status, body: r.body, headers: r.headers }));
|
|
3371
|
+
expect(seen.length).toBeGreaterThan(0);
|
|
2182
3372
|
});
|
|
2183
3373
|
}
|
|
2184
3374
|
});
|
|
2185
3375
|
`;
|
|
2186
|
-
return { status: "generated", file, content, functionName: fn.name };
|
|
2187
3376
|
}
|
|
3377
|
+
function renamePair(site, fix2) {
|
|
3378
|
+
const paths = site.surface.fields;
|
|
3379
|
+
const rename = (p) => {
|
|
3380
|
+
for (const { from, to } of fix2.fields ?? []) {
|
|
3381
|
+
if (!/[.[]/.test(from)) {
|
|
3382
|
+
const segs = p.split(".");
|
|
3383
|
+
const i = segs.findIndex((seg) => seg.replace(/\[\]/g, "") === from);
|
|
3384
|
+
if (i >= 0) return [...segs.slice(0, i), segs[i].replace(from, to), ...segs.slice(i + 1)].join(".");
|
|
3385
|
+
} else if (p === from || p.startsWith(`${from}.`) || p.startsWith(`${from}[]`)) return to + p.slice(from.length);
|
|
3386
|
+
}
|
|
3387
|
+
return p;
|
|
3388
|
+
};
|
|
3389
|
+
const oldValues = {};
|
|
3390
|
+
const newValues = {};
|
|
3391
|
+
for (const { field, values } of fix2.enums ?? []) {
|
|
3392
|
+
const compared = site.surface.enums.find((e) => e.field === field)?.values ?? [];
|
|
3393
|
+
const pick = values.find((v) => compared.includes(v.from)) ?? values[0];
|
|
3394
|
+
oldValues[field] = pick.from;
|
|
3395
|
+
newValues[rename(field)] = pick.to;
|
|
3396
|
+
}
|
|
3397
|
+
return { before: bodyFromPaths(paths, oldValues), after: bodyFromPaths(paths.map(rename), newValues) };
|
|
3398
|
+
}
|
|
3399
|
+
function describeFix(fix2) {
|
|
3400
|
+
const parts = [];
|
|
3401
|
+
if (fix2.path) parts.push(`${fix2.path.to} instead of ${fix2.path.from}`);
|
|
3402
|
+
if (fix2.query) parts.push(`${fix2.query.name}=${fix2.query.value}`);
|
|
3403
|
+
if (fix2.header) parts.push(`${fix2.header.name}: ${fix2.header.value}`);
|
|
3404
|
+
return parts.join(", ");
|
|
3405
|
+
}
|
|
3406
|
+
var AXIOS_FAKE = `vi.mock("axios", () => {
|
|
3407
|
+
class AxiosError extends Error {
|
|
3408
|
+
isAxiosError = true;
|
|
3409
|
+
constructor(message: string, readonly response?: unknown, readonly status?: number) {
|
|
3410
|
+
super(message);
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
const build = (defaults: any = {}): any => {
|
|
3414
|
+
const send = async (config: any) => {
|
|
3415
|
+
const c = { ...defaults, ...config, headers: { ...defaults.headers, ...config.headers }, params: { ...defaults.params, ...config.params } };
|
|
3416
|
+
let url = String(c.url ?? "");
|
|
3417
|
+
if (c.baseURL && !/^[a-z]+:\\/\\//i.test(url)) url = String(c.baseURL).replace(/\\/+$/, "") + "/" + url.replace(/^\\/+/, "");
|
|
3418
|
+
const qs = new URLSearchParams(Object.entries(c.params).filter(([, v]) => v !== undefined && v !== null).map(([k, v]) => [k, String(v)])).toString();
|
|
3419
|
+
if (qs) url += (url.includes("?") ? "&" : "?") + qs;
|
|
3420
|
+
const res = shapi.respond({ method: String(c.method ?? "get").toUpperCase(), url, headers: shapi.lower(c.headers) });
|
|
3421
|
+
const response = { status: res.status, statusText: "", headers: res.headers, data: res.body, config: c };
|
|
3422
|
+
if (res.status < 200 || res.status >= 300) throw new AxiosError(\`Request failed with status code \${res.status}\`, response, res.status);
|
|
3423
|
+
return response;
|
|
3424
|
+
};
|
|
3425
|
+
const instance: any = (a: any, b?: any) => send(typeof a === "string" ? { ...b, url: a } : a);
|
|
3426
|
+
instance.request = (c: any) => send(c);
|
|
3427
|
+
for (const m of ["get", "delete", "head", "options"]) instance[m] = (url: string, c: any = {}) => send({ ...c, url, method: m });
|
|
3428
|
+
for (const m of ["post", "put", "patch"]) instance[m] = (url: string, data: unknown, c: any = {}) => send({ ...c, url, data, method: m });
|
|
3429
|
+
instance.create = (d: any = {}) => build({ ...defaults, ...d, headers: { ...defaults.headers, ...d.headers } });
|
|
3430
|
+
instance.defaults = defaults;
|
|
3431
|
+
instance.interceptors = { request: { use: () => 0, eject: () => {} }, response: { use: () => 0, eject: () => {} } };
|
|
3432
|
+
instance.isAxiosError = (e: any) => Boolean(e?.isAxiosError);
|
|
3433
|
+
return instance;
|
|
3434
|
+
};
|
|
3435
|
+
const axios = build();
|
|
3436
|
+
return { default: axios, create: axios.create, isAxiosError: axios.isAxiosError, AxiosError };
|
|
3437
|
+
});
|
|
3438
|
+
`;
|
|
3439
|
+
var GOT_FAKE = `vi.mock("got", () => {
|
|
3440
|
+
class HTTPError extends Error {
|
|
3441
|
+
constructor(readonly response: any) {
|
|
3442
|
+
super(\`Response code \${response.statusCode}\`);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
const build = (defaults: any = {}): any => {
|
|
3446
|
+
const call = (input: any, options: any = {}) => {
|
|
3447
|
+
const fromObject = typeof input === "object" && !(input instanceof URL);
|
|
3448
|
+
const o = { ...defaults, ...(fromObject ? input : options), headers: { ...defaults.headers, ...(fromObject ? input.headers : options.headers) } };
|
|
3449
|
+
let url = fromObject ? String(o.url ?? "") : String(input);
|
|
3450
|
+
if (o.prefixUrl) url = String(o.prefixUrl).replace(/\\/+$/, "") + "/" + url.replace(/^\\/+/, "");
|
|
3451
|
+
const sp = o.searchParams;
|
|
3452
|
+
const qs = sp ? new URLSearchParams(typeof sp === "string" || sp instanceof URLSearchParams ? sp : Object.entries(sp).map(([k, v]) => [k, String(v)])).toString() : "";
|
|
3453
|
+
if (qs) url += (url.includes("?") ? "&" : "?") + qs;
|
|
3454
|
+
const res = shapi.respond({ method: String(o.method ?? "GET").toUpperCase(), url, headers: shapi.lower(o.headers) });
|
|
3455
|
+
const text = res.body === null ? "" : JSON.stringify(res.body);
|
|
3456
|
+
const response = { statusCode: res.status, headers: res.headers, body: o.responseType === "json" ? res.body : text, url };
|
|
3457
|
+
const failed = (o.throwHttpErrors ?? true) && res.status >= 400;
|
|
3458
|
+
const settle = <T,>(value: T): Promise<T> => (failed ? Promise.reject(new HTTPError(response)) : Promise.resolve(value));
|
|
3459
|
+
const promise: any = settle(response);
|
|
3460
|
+
promise.catch(() => {});
|
|
3461
|
+
promise.json = () => settle(res.body);
|
|
3462
|
+
promise.text = () => settle(text);
|
|
3463
|
+
promise.buffer = () => settle(Buffer.from(text));
|
|
3464
|
+
return promise;
|
|
3465
|
+
};
|
|
3466
|
+
const got: any = (input: any, options?: any) => call(input, options);
|
|
3467
|
+
for (const m of ["get", "post", "put", "patch", "delete", "head"]) got[m] = (input: any, options: any = {}) => call(input, { ...options, method: m.toUpperCase() });
|
|
3468
|
+
got.extend = (...opts: any[]) => build(Object.assign({}, defaults, ...opts));
|
|
3469
|
+
return got;
|
|
3470
|
+
};
|
|
3471
|
+
return { default: build(), HTTPError };
|
|
3472
|
+
});
|
|
3473
|
+
`;
|
|
2188
3474
|
function response(name, shape) {
|
|
2189
|
-
const
|
|
3475
|
+
const body3 = synthesizeBody(shape);
|
|
2190
3476
|
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(
|
|
3477
|
+
return ` { name: ${JSON.stringify(name)}, status: ${shape.status}, headers: ${JSON.stringify(headers)}, body: ${JSON.stringify(body3)} }`;
|
|
2192
3478
|
}
|
|
2193
3479
|
function exportedFunction(call) {
|
|
2194
3480
|
for (const anc of call.getAncestors()) {
|
|
2195
|
-
if (
|
|
2196
|
-
if (
|
|
3481
|
+
if (Node5.isFunctionDeclaration(anc) && anc.isExported() && anc.getName()) return { name: anc.getName(), params: anc.getParameters() };
|
|
3482
|
+
if (Node5.isArrowFunction(anc) || Node5.isFunctionExpression(anc)) {
|
|
2197
3483
|
const decl = anc.getParent();
|
|
2198
|
-
if (
|
|
3484
|
+
if (Node5.isVariableDeclaration(decl) && decl.getVariableStatement()?.isExported()) return { name: decl.getName(), params: anc.getParameters() };
|
|
2199
3485
|
}
|
|
2200
3486
|
}
|
|
2201
3487
|
return void 0;
|
|
@@ -2207,22 +3493,22 @@ function argument(p, sf) {
|
|
|
2207
3493
|
function valueFor(type, name, sf, depth) {
|
|
2208
3494
|
if (depth > 4) return "undefined as never";
|
|
2209
3495
|
switch (type.getKind()) {
|
|
2210
|
-
case
|
|
3496
|
+
case SyntaxKind6.StringKeyword:
|
|
2211
3497
|
return JSON.stringify(`shapi-${name}`);
|
|
2212
|
-
case
|
|
3498
|
+
case SyntaxKind6.NumberKeyword:
|
|
2213
3499
|
return "1";
|
|
2214
|
-
case
|
|
3500
|
+
case SyntaxKind6.BooleanKeyword:
|
|
2215
3501
|
return "true";
|
|
2216
3502
|
}
|
|
2217
|
-
if (
|
|
2218
|
-
if (
|
|
3503
|
+
if (Node5.isTypeLiteral(type)) return objectFrom(type.getProperties(), sf, depth);
|
|
3504
|
+
if (Node5.isTypeReference(type)) {
|
|
2219
3505
|
const ref = type.getTypeName().getText();
|
|
2220
3506
|
const iface = sf.getInterface(ref);
|
|
2221
3507
|
if (iface) return objectFrom(iface.getProperties(), sf, depth);
|
|
2222
3508
|
const alias = sf.getTypeAlias(ref)?.getTypeNode();
|
|
2223
3509
|
if (alias) return valueFor(alias, name, sf, depth + 1);
|
|
2224
3510
|
}
|
|
2225
|
-
if (
|
|
3511
|
+
if (Node5.isArrayTypeNode(type)) return "[]";
|
|
2226
3512
|
return "undefined as never";
|
|
2227
3513
|
}
|
|
2228
3514
|
function objectFrom(props, sf, depth) {
|
|
@@ -2242,13 +3528,13 @@ import path10 from "path";
|
|
|
2242
3528
|
|
|
2243
3529
|
// packages/repair/src/runner.ts
|
|
2244
3530
|
import { spawnSync } from "child_process";
|
|
2245
|
-
import { existsSync } from "fs";
|
|
2246
|
-
import { createRequire } from "module";
|
|
3531
|
+
import { existsSync as existsSync2 } from "fs";
|
|
3532
|
+
import { createRequire as createRequire2 } from "module";
|
|
2247
3533
|
import path8 from "path";
|
|
2248
3534
|
function vitestCli(dir) {
|
|
2249
3535
|
const local = path8.join(dir, "node_modules", "vitest", "vitest.mjs");
|
|
2250
|
-
if (
|
|
2251
|
-
const require2 =
|
|
3536
|
+
if (existsSync2(local)) return local;
|
|
3537
|
+
const require2 = createRequire2(import.meta.url);
|
|
2252
3538
|
return path8.join(path8.dirname(require2.resolve("vitest/package.json")), "vitest.mjs");
|
|
2253
3539
|
}
|
|
2254
3540
|
function runTestFile(dir, file, timeoutMs = 12e4) {
|
|
@@ -2272,7 +3558,7 @@ function runFailure(output) {
|
|
|
2272
3558
|
}
|
|
2273
3559
|
|
|
2274
3560
|
// packages/repair/src/workspace.ts
|
|
2275
|
-
import { cpSync, existsSync as
|
|
3561
|
+
import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync4, mkdtempSync, readFileSync as readFileSync5, symlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
2276
3562
|
import os from "os";
|
|
2277
3563
|
import path9 from "path";
|
|
2278
3564
|
var COPY_SKIP = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "coverage", ".shapi"]);
|
|
@@ -2283,12 +3569,12 @@ function localWorkspace(repoDir) {
|
|
|
2283
3569
|
filter: (src) => !COPY_SKIP.has(path9.basename(src)) || path9.resolve(src) === path9.resolve(repoDir)
|
|
2284
3570
|
});
|
|
2285
3571
|
const modules = path9.join(repoDir, "node_modules");
|
|
2286
|
-
if (
|
|
3572
|
+
if (existsSync3(modules)) symlinkSync(modules, path9.join(dir, "node_modules"), "junction");
|
|
2287
3573
|
return {
|
|
2288
3574
|
location: dir,
|
|
2289
3575
|
async read(file) {
|
|
2290
3576
|
const abs = path9.join(dir, file);
|
|
2291
|
-
return
|
|
3577
|
+
return existsSync3(abs) ? readFileSync5(abs, "utf8") : void 0;
|
|
2292
3578
|
},
|
|
2293
3579
|
async write(file, content) {
|
|
2294
3580
|
const abs = path9.join(dir, file);
|
|
@@ -2306,10 +3592,13 @@ var PROTECTED = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "covera
|
|
|
2306
3592
|
async function repairCallSite(input) {
|
|
2307
3593
|
const { advisory, callSite } = input;
|
|
2308
3594
|
if (!input.workspace && !input.repoDir) throw new Error("repairCallSite needs a workspace or a repoDir");
|
|
3595
|
+
if (!/\.(c|m)?[jt]sx?$/.test(callSite.file)) {
|
|
3596
|
+
return { status: "unsupported", changes: [], attempts: [], reason: `fixes support TypeScript and JavaScript; ${callSite.file} is scanned only` };
|
|
3597
|
+
}
|
|
2309
3598
|
const ws = input.workspace ?? localWorkspace(input.repoDir);
|
|
2310
3599
|
const original = await ws.read(callSite.file);
|
|
2311
3600
|
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 });
|
|
3601
|
+
const test = generateContractTest({ callSite, advisory, before: input.before, after: input.after, sourceText: original, file: input.testFile });
|
|
2313
3602
|
if (test.status !== "generated") return { status: "unsupported", changes: [], attempts: [], reason: test.reason };
|
|
2314
3603
|
await ws.write(test.file, test.content);
|
|
2315
3604
|
const testChange = { file: test.file, before: "", after: test.content, notes: ["generated contract test"] };
|
|
@@ -2398,6 +3687,42 @@ function rejected(ws, test, baseline, attempts) {
|
|
|
2398
3687
|
async function restore(ws, changes) {
|
|
2399
3688
|
for (const c of changes) await ws.write(c.file, c.before);
|
|
2400
3689
|
}
|
|
3690
|
+
async function repairAdvisory(input) {
|
|
3691
|
+
if (!input.workspace && !input.repoDir) throw new Error("repairAdvisory needs a workspace or a repoDir");
|
|
3692
|
+
const ws = input.workspace ?? localWorkspace(input.repoDir);
|
|
3693
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3694
|
+
const order = input.callSites.filter((s) => {
|
|
3695
|
+
const key = `${s.file}:${s.line}:${s.column}`;
|
|
3696
|
+
return !seen.has(key) && Boolean(seen.add(key));
|
|
3697
|
+
}).sort((a, b) => a.file.localeCompare(b.file) || b.line - a.line || b.column - a.column);
|
|
3698
|
+
const originals = /* @__PURE__ */ new Map();
|
|
3699
|
+
const sites = [];
|
|
3700
|
+
for (const [i, callSite] of order.entries()) {
|
|
3701
|
+
if (!originals.has(callSite.file)) originals.set(callSite.file, await ws.read(callSite.file) ?? "");
|
|
3702
|
+
const testFile = `test/shapi/${input.advisory.id}${i === 0 ? "" : `.${i + 1}`}.test.ts`;
|
|
3703
|
+
const result = await repairCallSite({ ...input, workspace: ws, callSite, testFile });
|
|
3704
|
+
for (const c of result.changes) if (c.before && !originals.has(c.file)) originals.set(c.file, c.before);
|
|
3705
|
+
sites.push({ callSite, result });
|
|
3706
|
+
}
|
|
3707
|
+
const fixed = sites.filter((s) => s.result.status === "fixed");
|
|
3708
|
+
const needed = sites.filter((s) => s.result.status !== "already-handled");
|
|
3709
|
+
const tests = new Set(fixed.map((s) => s.result.test?.file));
|
|
3710
|
+
const sourceFiles = [...new Set(fixed.flatMap((s) => s.result.changes.filter((c) => !tests.has(c.file)).map((c) => c.file)))].sort();
|
|
3711
|
+
const changes = [];
|
|
3712
|
+
for (const file of sourceFiles) {
|
|
3713
|
+
const notes = fixed.flatMap((s) => s.result.changes.filter((c) => c.file === file).flatMap((c) => c.notes));
|
|
3714
|
+
changes.push({ file, before: originals.get(file) ?? "", after: await ws.read(file) ?? "", notes });
|
|
3715
|
+
}
|
|
3716
|
+
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"] });
|
|
3717
|
+
const methods = new Set(fixed.map((s) => s.result.method));
|
|
3718
|
+
return {
|
|
3719
|
+
status: fixed.length === 0 ? "none" : fixed.length === needed.length ? "fixed" : "partial",
|
|
3720
|
+
sites,
|
|
3721
|
+
changes,
|
|
3722
|
+
...fixed.length ? { method: methods.has("llm") ? "llm" : "recipe" } : {},
|
|
3723
|
+
sandbox: ws.location
|
|
3724
|
+
};
|
|
3725
|
+
}
|
|
2401
3726
|
|
|
2402
3727
|
// packages/repair/src/llm.ts
|
|
2403
3728
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -2510,7 +3835,8 @@ ${req.failingOutput.slice(-6e3)}
|
|
|
2510
3835
|
|
|
2511
3836
|
// packages/repair/src/index.ts
|
|
2512
3837
|
var RECIPES = {
|
|
2513
|
-
[statusCodeAcceptSet.id]: statusCodeAcceptSet
|
|
3838
|
+
[statusCodeAcceptSet.id]: statusCodeAcceptSet,
|
|
3839
|
+
[applyFix.id]: applyFix
|
|
2514
3840
|
};
|
|
2515
3841
|
|
|
2516
3842
|
// packages/github-app/src/pr.ts
|
|
@@ -2651,29 +3977,31 @@ function prTitle(advisory) {
|
|
|
2651
3977
|
function prBranch(advisory) {
|
|
2652
3978
|
return `shapi/${advisory.id}`;
|
|
2653
3979
|
}
|
|
2654
|
-
function renderPrBody({ advisory,
|
|
2655
|
-
const
|
|
2656
|
-
const
|
|
2657
|
-
const
|
|
3980
|
+
function renderPrBody({ advisory, repair, shapeSource }) {
|
|
3981
|
+
const fixed = repair.sites.filter((s) => s.result.status === "fixed");
|
|
3982
|
+
const tests = new Set(fixed.map((s) => s.result.test?.file));
|
|
3983
|
+
const notes = repair.changes.filter((c) => !tests.has(c.file)).flatMap((c) => (c.notes.length ? c.notes : [`\`${c.file}\``]).map((n) => `- ${n}`));
|
|
3984
|
+
const method = repair.method === "llm" ? "Claude, verified by the contract tests" : `deterministic recipe \`${advisory.recipeId}\``;
|
|
2658
3985
|
const evidence = advisory.evidence;
|
|
3986
|
+
const where = (s) => `\`${s.callSite.file}:${s.callSite.line}\` (${s.callSite.method} ${s.callSite.endpoint})`;
|
|
3987
|
+
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
3988
|
const lines = [
|
|
2660
3989
|
`**${advisory.provider}** changed \`${advisory.endpoint.method} ${advisory.endpoint.path}\`: ${advisory.title}.`,
|
|
2661
3990
|
"",
|
|
2662
3991
|
advisory.summary,
|
|
2663
3992
|
"",
|
|
2664
|
-
|
|
2665
|
-
|
|
3993
|
+
`### Affected call sites (${fixed.length} of ${repair.sites.length} fixed here)`,
|
|
3994
|
+
...repair.sites.map(siteLine),
|
|
2666
3995
|
"",
|
|
2667
3996
|
`### Change (${method})`,
|
|
2668
|
-
...notes
|
|
3997
|
+
...notes,
|
|
2669
3998
|
"",
|
|
2670
|
-
|
|
2671
|
-
|
|
3999
|
+
`### Contract test${fixed.length === 1 ? "" : "s"}`,
|
|
4000
|
+
"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
4001
|
"",
|
|
2673
|
-
"|
|
|
2674
|
-
"
|
|
2675
|
-
|
|
2676
|
-
`| After this change | ${summary(result.attempts.find((a) => a.outcome === "passed")?.run)} |`,
|
|
4002
|
+
"| Test | Before this change | After this change |",
|
|
4003
|
+
"|---|---|---|",
|
|
4004
|
+
...fixed.map((s) => `| \`${s.result.test?.file}\` | ${summary(s.result.baseline)} | ${summary(s.result.attempts.find((a) => a.outcome === "passed")?.run)} |`),
|
|
2677
4005
|
"",
|
|
2678
4006
|
"### Evidence",
|
|
2679
4007
|
"| | |",
|
|
@@ -2685,13 +4013,13 @@ function renderPrBody({ advisory, callSite, result, affectedInRepo, shapeSource
|
|
|
2685
4013
|
`| Provider notice | ${advisory.publishedNotice ? `[${advisory.publishedNotice.date ?? "link"}](${advisory.publishedNotice.url})` : "none (undocumented change)"} |`,
|
|
2686
4014
|
`| Shapes from | ${shapeSource} |`,
|
|
2687
4015
|
...evidence ? [`| Observed | ${evidence.before.status} at ${evidence.before.observedAt} \u2192 ${evidence.after.status} at ${evidence.after.observedAt} (${evidence.source}) |`] : [],
|
|
2688
|
-
`| Blast radius | ${
|
|
4016
|
+
`| Blast radius | ${repair.sites.length} affected call site${repair.sites.length === 1 ? "" : "s"} in this repo |`,
|
|
2689
4017
|
`| Sources | ${advisory.sources.map((s, i) => `[${i + 1}](${s})`).join(" ")} |`,
|
|
2690
4018
|
"",
|
|
2691
4019
|
`Migration note: ${advisory.migrationNote}`,
|
|
2692
4020
|
"",
|
|
2693
4021
|
"---",
|
|
2694
|
-
"Opened by [SHAPI](https://
|
|
4022
|
+
"Opened by [SHAPI](https://shapi.dev). A change is kept only when its contract test failed before it and passes after it."
|
|
2695
4023
|
];
|
|
2696
4024
|
return lines.join("\n") + "\n";
|
|
2697
4025
|
}
|
|
@@ -2712,7 +4040,7 @@ import { Octokit as Octokit2 } from "@octokit/rest";
|
|
|
2712
4040
|
var ResilientOctokit = Octokit2.plugin(throttling, retry);
|
|
2713
4041
|
|
|
2714
4042
|
// packages/github-app/src/snapshot.ts
|
|
2715
|
-
import { Parser } from "tar";
|
|
4043
|
+
import { Parser as Parser2 } from "tar";
|
|
2716
4044
|
|
|
2717
4045
|
// packages/cli/src/scan.ts
|
|
2718
4046
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -2721,16 +4049,19 @@ import os2 from "os";
|
|
|
2721
4049
|
import path11 from "path";
|
|
2722
4050
|
|
|
2723
4051
|
// packages/cli/src/paths.ts
|
|
2724
|
-
import { existsSync as
|
|
2725
|
-
import { fileURLToPath } from "url";
|
|
4052
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4053
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2726
4054
|
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) =>
|
|
4055
|
+
const candidates = [new URL(`../${published}`, import.meta.url), new URL(`../../../${monorepo}`, import.meta.url)].map((u) => fileURLToPath2(u));
|
|
4056
|
+
return candidates.find((p) => existsSync4(p)) ?? candidates[candidates.length - 1];
|
|
2729
4057
|
}
|
|
2730
4058
|
|
|
2731
4059
|
// packages/cli/src/scan.ts
|
|
2732
|
-
var VERSION = "0.
|
|
4060
|
+
var VERSION = "0.3.0";
|
|
2733
4061
|
var DEFAULT_ADVISORIES = bundledDir("advisories", "advisories");
|
|
4062
|
+
async function preparePython() {
|
|
4063
|
+
await loadPythonParser().catch(() => void 0);
|
|
4064
|
+
}
|
|
2734
4065
|
function scan(target, opts) {
|
|
2735
4066
|
const started = performance.now();
|
|
2736
4067
|
const failOn = parseFailOn(opts.failOn ?? "critical");
|
|
@@ -2753,9 +4084,12 @@ function scan(target, opts) {
|
|
|
2753
4084
|
const advisories = loadAdvisories(opts.advisories ?? DEFAULT_ADVISORIES);
|
|
2754
4085
|
const match = matchAdvisories(lock, advisories, { now });
|
|
2755
4086
|
const failing = failOn === "none" ? [] : match.affected.filter((f) => SEVERITY_RANK[f.advisory.severity] <= SEVERITY_RANK[failOn]);
|
|
4087
|
+
const where = opts.repo ? `--repo ${opts.repo}` : label2;
|
|
2756
4088
|
return {
|
|
2757
4089
|
label: label2,
|
|
4090
|
+
fixTarget: /\s/.test(where) ? JSON.stringify(where) : where,
|
|
2758
4091
|
filesScanned: extracted.filesScanned,
|
|
4092
|
+
skipped: extracted.skipped,
|
|
2759
4093
|
lock,
|
|
2760
4094
|
match,
|
|
2761
4095
|
elapsedMs: Math.round(performance.now() - started),
|
|
@@ -2787,6 +4121,7 @@ function toJsonReport(o) {
|
|
|
2787
4121
|
filesScanned: o.filesScanned,
|
|
2788
4122
|
affected: o.match.affected.map(finding),
|
|
2789
4123
|
exposed: o.match.exposed.map(finding),
|
|
4124
|
+
possible: o.match.possible.map(finding),
|
|
2790
4125
|
upcoming: o.match.upcoming.map((u) => ({
|
|
2791
4126
|
advisory: u.advisory.id,
|
|
2792
4127
|
provider: u.advisory.provider,
|
|
@@ -2822,24 +4157,31 @@ async function fix(target, opts, deps) {
|
|
|
2822
4157
|
if (affected.length === 0) deps.log("No affected call sites: nothing to fix.");
|
|
2823
4158
|
const llm = opts.llm ? claudeFixer() : void 0;
|
|
2824
4159
|
const outcomes = [];
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
const
|
|
4160
|
+
const byAdvisory = /* @__PURE__ */ new Map();
|
|
4161
|
+
for (const f of affected) byAdvisory.set(f.advisory.id, [...byAdvisory.get(f.advisory.id) ?? [], f]);
|
|
4162
|
+
for (const findings of byAdvisory.values()) {
|
|
4163
|
+
const advisory = findings[0].advisory;
|
|
4164
|
+
const shapes = advisory.fix ? { source: FIX_SOURCE } : findShapes(advisory, opts);
|
|
2829
4165
|
if (!shapes) {
|
|
2830
4166
|
const skipped = "no before/after shapes for this endpoint (record them with the canary, or add a recording)";
|
|
2831
|
-
|
|
2832
|
-
|
|
4167
|
+
for (const finding of findings) {
|
|
4168
|
+
deps.log(`SKIP ${finding.callSite.file}:${finding.callSite.line} ${advisory.id}: ${skipped}`);
|
|
4169
|
+
outcomes.push({ finding, skipped });
|
|
4170
|
+
}
|
|
2833
4171
|
continue;
|
|
2834
4172
|
}
|
|
2835
|
-
const
|
|
2836
|
-
const
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
4173
|
+
const repair = await repairAdvisory({ workspace: localWorkspace(dir), advisory, callSites: findings.map((f) => f.callSite), before: shapes.before, after: shapes.after, recipes: RECIPES, llm });
|
|
4174
|
+
const mine = [];
|
|
4175
|
+
for (const { callSite, result } of repair.sites) {
|
|
4176
|
+
const finding = findings.find((f) => f.callSite.file === callSite.file && f.callSite.line === callSite.line && f.callSite.column === callSite.column);
|
|
4177
|
+
mine.push({ finding, shapeSource: shapes.source, result });
|
|
4178
|
+
deps.log(`${result.status.toUpperCase().padEnd(15)} ${callSite.file}:${callSite.line} ${advisory.id}${result.method ? ` via ${result.method}` : ""}${result.reason ? `: ${result.reason}` : ""}`);
|
|
4179
|
+
for (const a of result.attempts) deps.log(` ${a.method}: ${a.outcome} (${a.detail})`);
|
|
4180
|
+
}
|
|
4181
|
+
outcomes.push(...mine);
|
|
4182
|
+
if (!repair.changes.length) continue;
|
|
4183
|
+
for (const c of repair.changes) deps.log(` ${c.before ? "M" : "A"} ${c.file}`);
|
|
4184
|
+
const files = repair.changes.map((c) => ({ path: c.file, content: c.after }));
|
|
2843
4185
|
if (opts.write) {
|
|
2844
4186
|
for (const f of files) {
|
|
2845
4187
|
mkdirSync5(path12.dirname(path12.join(dir, f.path)), { recursive: true });
|
|
@@ -2848,22 +4190,18 @@ async function fix(target, opts, deps) {
|
|
|
2848
4190
|
deps.log(` wrote ${files.length} files into ${dir}`);
|
|
2849
4191
|
}
|
|
2850
4192
|
if (opts.pr && remote) {
|
|
4193
|
+
const fixed = repair.sites.filter((s) => s.result.status === "fixed");
|
|
2851
4194
|
const meta = {
|
|
2852
4195
|
title: prTitle(advisory),
|
|
2853
4196
|
branch: prBranch(advisory),
|
|
2854
4197
|
commitMessage: `fix: ${advisory.title}
|
|
2855
4198
|
|
|
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
|
-
})
|
|
4199
|
+
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.`,
|
|
4200
|
+
body: renderPrBody({ advisory, repair, shapeSource: shapes.source })
|
|
2864
4201
|
};
|
|
2865
|
-
|
|
2866
|
-
|
|
4202
|
+
const pr = await (deps.deliver ?? deliverToGitHub)(remote, files, meta);
|
|
4203
|
+
for (const o of mine) if (o.result?.status === "fixed") o.pr = pr;
|
|
4204
|
+
deps.log(` pull request: ${pr.url}`);
|
|
2867
4205
|
}
|
|
2868
4206
|
}
|
|
2869
4207
|
return outcomes;
|
|
@@ -2878,8 +4216,9 @@ async function deliverToGitHub(repo, files, meta) {
|
|
|
2878
4216
|
if (outcome.action === "skipped") throw new Error(`not delivered: ${outcome.reason}${outcome.url ? ` (${outcome.url})` : ""}`);
|
|
2879
4217
|
return { url: outcome.url, number: outcome.number, branch: outcome.branch, commit: outcome.commit };
|
|
2880
4218
|
}
|
|
4219
|
+
var FIX_SOURCE = "built from the advisory's fix and the fields the code reads";
|
|
2881
4220
|
function findShapes(advisory, opts) {
|
|
2882
|
-
if (opts.db &&
|
|
4221
|
+
if (opts.db && existsSync5(opts.db)) {
|
|
2883
4222
|
const store = ShapeStore.open(opts.db);
|
|
2884
4223
|
try {
|
|
2885
4224
|
for (const e of store.endpoints()) {
|
|
@@ -2894,7 +4233,7 @@ function findShapes(advisory, opts) {
|
|
|
2894
4233
|
}
|
|
2895
4234
|
}
|
|
2896
4235
|
const dir = opts.recordings ?? DEFAULT_RECORDINGS;
|
|
2897
|
-
if (!
|
|
4236
|
+
if (!existsSync5(dir)) return void 0;
|
|
2898
4237
|
for (const name of readdirSync3(dir).filter((f) => f.endsWith(".json")).sort()) {
|
|
2899
4238
|
const recording = loadRecording(path12.join(dir, name));
|
|
2900
4239
|
if (recording.provider !== advisory.provider || !pathMatches(recording.endpoint, advisory.endpoint)) continue;
|
|
@@ -2916,19 +4255,26 @@ function originRepo(dir) {
|
|
|
2916
4255
|
|
|
2917
4256
|
// packages/cli/src/report.ts
|
|
2918
4257
|
import { styleText } from "util";
|
|
4258
|
+
var APP_URL = "https://app.shapi.dev";
|
|
4259
|
+
function title(color) {
|
|
4260
|
+
const paint = (style, s) => color ? styleText(style, s) : s;
|
|
4261
|
+
return `${paint("bold", "SHAPI")} ${paint("dim", "third-party API changes, caught and fixed")}`;
|
|
4262
|
+
}
|
|
2919
4263
|
function renderText(o, color) {
|
|
2920
4264
|
const paint = (style, s) => color ? styleText(style, s) : s;
|
|
2921
4265
|
const out = [];
|
|
2922
4266
|
const { lock, match } = o;
|
|
2923
4267
|
const providers = lock.providers;
|
|
2924
4268
|
const unresolved = lock.callSites.filter((c) => !c.resolved).length;
|
|
2925
|
-
out.push(
|
|
4269
|
+
out.push(title(color));
|
|
4270
|
+
out.push(`${paint("bold", "scan")} ${o.label}`);
|
|
2926
4271
|
out.push(
|
|
2927
4272
|
paint(
|
|
2928
4273
|
"dim",
|
|
2929
4274
|
[plural(o.filesScanned, "file"), plural(lock.callSites.length, "call site"), plural(providers.length, "provider"), `${o.elapsedMs} ms`].join(" \xB7 ")
|
|
2930
4275
|
)
|
|
2931
4276
|
);
|
|
4277
|
+
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
4278
|
out.push("");
|
|
2933
4279
|
out.push(paint("bold", "Providers"));
|
|
2934
4280
|
if (providers.length === 0) out.push(" none found");
|
|
@@ -2942,6 +4288,11 @@ function renderText(o, color) {
|
|
|
2942
4288
|
if (match.affected.length === 0) out.push(" none");
|
|
2943
4289
|
for (const f of match.affected) out.push(...renderFinding(f, paint));
|
|
2944
4290
|
out.push("");
|
|
4291
|
+
if (match.possible.length > 0) {
|
|
4292
|
+
out.push(paint("bold", `Possibly affected, check these (${match.possible.length})`));
|
|
4293
|
+
for (const f of match.possible) out.push(...renderFinding(f, paint));
|
|
4294
|
+
out.push("");
|
|
4295
|
+
}
|
|
2945
4296
|
if (match.exposed.length > 0) {
|
|
2946
4297
|
out.push(paint("bold", `Exposed, not affected (${match.exposed.length})`));
|
|
2947
4298
|
for (const f of match.exposed) {
|
|
@@ -2959,6 +4310,10 @@ function renderText(o, color) {
|
|
|
2959
4310
|
out.push("");
|
|
2960
4311
|
out.push(paint("dim", `${plural(unresolved, "call site")} could not be fully resolved (see --json or --out api.lock).`));
|
|
2961
4312
|
}
|
|
4313
|
+
out.push("");
|
|
4314
|
+
out.push(paint("bold", "Next"));
|
|
4315
|
+
if (match.affected.length > 0) out.push(` npx shapi-cli fix ${o.fixTarget} ${paint("dim", "fix the affected call sites test-first (nothing changes until you add --write or --pr)")}`);
|
|
4316
|
+
out.push(` ${APP_URL} ${paint("dim", "install the GitHub App: every push scanned, verified fix PRs, and a dashboard")}`);
|
|
2962
4317
|
return out.join("\n") + "\n";
|
|
2963
4318
|
}
|
|
2964
4319
|
var SEVERITY_STYLE = { critical: "red", high: "yellow", medium: "cyan", low: "gray" };
|
|
@@ -2988,7 +4343,12 @@ var processIo = {
|
|
|
2988
4343
|
};
|
|
2989
4344
|
async function run(argv, io = processIo, fixDeps = {}) {
|
|
2990
4345
|
let exitCode = 0;
|
|
2991
|
-
|
|
4346
|
+
await preparePython();
|
|
4347
|
+
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).addHelpText("beforeAll", `${title(Boolean(io.isTTY))}
|
|
4348
|
+
`).addHelpText("afterAll", `
|
|
4349
|
+
Run it without installing: npx shapi-cli scan .
|
|
4350
|
+
Fix PRs and a dashboard on every push: install the GitHub App at ${APP_URL}
|
|
4351
|
+
`).exitOverride().configureOutput({ writeOut: io.stdout, writeErr: io.stderr });
|
|
2992
4352
|
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
4353
|
const outcome = scan(opts.repo ? void 0 : target, opts);
|
|
2994
4354
|
if (opts.json) io.stdout(JSON.stringify(toJsonReport(outcome), null, 2) + "\n");
|
|
@@ -2996,8 +4356,13 @@ async function run(argv, io = processIo, fixDeps = {}) {
|
|
|
2996
4356
|
exitCode = outcome.exitCode;
|
|
2997
4357
|
});
|
|
2998
4358
|
program.command("fix").description("Repair affected call sites test-first: generate a contract test, apply the recipe (or Claude), keep the change only if the test passes").argument("[path]", "repository to fix", ".").option("--repo <url>", "clone a GitHub repository (shallow) and fix it; with --pr the PR goes to that repository").option("--advisory <id>", "only this advisory").option("--advisories <dir>", "advisory directory (default: the bundled advisories/)").option("--db <file>", "shape store with canary observations (checked before recordings)", ".shapi/shapes.db").option("--recordings <dir>", "recorded response sequences (default: the bundled fixtures/recordings/)").option("--llm", "ask Claude when no recipe fixes the call site (needs ANTHROPIC_API_KEY)").option("--write", "write the fix and the contract test into the local repository").option("--pr", "open a pull request with the fix and the contract test").addOption(new Option("--now <date>", "evaluate deadlines as of this date").hideHelp()).action(async (target, opts) => {
|
|
4359
|
+
io.stdout(`${title(Boolean(io.isTTY))}
|
|
4360
|
+
`);
|
|
2999
4361
|
const outcomes = await fix(opts.repo ? void 0 : target, opts, { log: (l) => io.stdout(`${l}
|
|
3000
4362
|
`), ...fixDeps });
|
|
4363
|
+
if (outcomes.some((o) => o.result?.status === "fixed") && !opts.write && !opts.pr) io.stdout("\nNothing was changed. Add --write to keep these fixes, or --pr to open pull requests.\n");
|
|
4364
|
+
io.stdout(`Fix PRs and a dashboard on every push: install the GitHub App at ${APP_URL}
|
|
4365
|
+
`);
|
|
3001
4366
|
exitCode = outcomes.every((o) => o.result?.status === "fixed" || o.result?.status === "already-handled") ? 0 : 1;
|
|
3002
4367
|
});
|
|
3003
4368
|
try {
|
|
@@ -3019,6 +4384,7 @@ export {
|
|
|
3019
4384
|
VERSION,
|
|
3020
4385
|
fix,
|
|
3021
4386
|
main,
|
|
4387
|
+
preparePython,
|
|
3022
4388
|
renderText,
|
|
3023
4389
|
run,
|
|
3024
4390
|
scan,
|