circle-ir 3.23.5 → 3.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4045,8 +4045,7 @@ async function loadLanguage(language, wasmPath) {
4045
4045
  if (loading) {
4046
4046
  return loading;
4047
4047
  }
4048
- const grammarName = language === "typescript" ? "javascript" : language;
4049
- const wasmModule = configuredLanguageModules[language] ?? configuredLanguageModules[grammarName];
4048
+ const wasmModule = configuredLanguageModules[language];
4050
4049
  if (wasmModule) {
4051
4050
  const loadPromise2 = (async () => {
4052
4051
  const lang = await Language.load(wasmModule);
@@ -4145,20 +4144,19 @@ async function getDefaultWasmPath() {
4145
4144
  return "node_modules/web-tree-sitter/web-tree-sitter.wasm";
4146
4145
  }
4147
4146
  async function getDefaultLanguagePath(language) {
4148
- const grammarName = language === "typescript" ? "javascript" : language;
4149
4147
  const mods = await getNodeModules();
4150
4148
  if (mods && moduleDir) {
4151
4149
  const packageRoot = mods.join(moduleDir, "..", "..");
4152
- const distWasmPath = mods.join(packageRoot, "dist", "wasm", `tree-sitter-${grammarName}.wasm`);
4150
+ const distWasmPath = mods.join(packageRoot, "dist", "wasm", `tree-sitter-${language}.wasm`);
4153
4151
  if (mods.existsSync(distWasmPath)) {
4154
4152
  return distWasmPath;
4155
4153
  }
4156
- const packageWasmPath = mods.join(packageRoot, "wasm", `tree-sitter-${grammarName}.wasm`);
4154
+ const packageWasmPath = mods.join(packageRoot, "wasm", `tree-sitter-${language}.wasm`);
4157
4155
  if (mods.existsSync(packageWasmPath)) {
4158
4156
  return packageWasmPath;
4159
4157
  }
4160
4158
  }
4161
- return `wasm/tree-sitter-${grammarName}.wasm`;
4159
+ return `wasm/tree-sitter-${language}.wasm`;
4162
4160
  }
4163
4161
 
4164
4162
  // src/core/extractors/meta.ts
@@ -4856,6 +4854,30 @@ function extractJSParameters(params) {
4856
4854
  annotations: [],
4857
4855
  line: child.startPosition.row + 1
4858
4856
  });
4857
+ } else if (child.type === "required_parameter" || child.type === "optional_parameter") {
4858
+ const patternNode = child.childForFieldName("pattern");
4859
+ if (!patternNode) continue;
4860
+ let paramName;
4861
+ if (patternNode.type === "identifier") {
4862
+ paramName = getNodeText(patternNode);
4863
+ } else if (patternNode.type === "rest_pattern" || patternNode.type === "rest_element") {
4864
+ const inner = patternNode.namedChildCount > 0 ? patternNode.namedChild(0) : null;
4865
+ if (!inner) continue;
4866
+ paramName = "..." + getNodeText(inner);
4867
+ } else {
4868
+ paramName = getNodeText(patternNode);
4869
+ }
4870
+ const typeNode = child.childForFieldName("type");
4871
+ let paramType = null;
4872
+ if (typeNode) {
4873
+ paramType = getNodeText(typeNode).replace(/^:\s*/, "");
4874
+ }
4875
+ parameters.push({
4876
+ name: paramName,
4877
+ type: paramType,
4878
+ annotations: [],
4879
+ line: child.startPosition.row + 1
4880
+ });
4859
4881
  }
4860
4882
  }
4861
4883
  return parameters;
@@ -13280,6 +13302,9 @@ var AnalysisPipeline = class {
13280
13302
  },
13281
13303
  addFinding(finding) {
13282
13304
  findings.push(finding);
13305
+ },
13306
+ getFindings() {
13307
+ return findings;
13283
13308
  }
13284
13309
  };
13285
13310
  for (const pass of this.passes) {
@@ -24991,6 +25016,271 @@ function detectHandler(graph, calls) {
24991
25016
  return false;
24992
25017
  }
24993
25018
 
25019
+ // src/analysis/passes/scan-secrets-pass.ts
25020
+ var TEST_PATH_RE3 = /(?:^|[\\/])(?:test|tests|spec|specs|__tests?__|__mocks?__|fixtures?|testdata)(?:[\\/]|$)/i;
25021
+ var TEST_FILENAME_RE = /(?:\.(?:test|spec)\.[cm]?[jt]sx?|_test\.go|_test\.py|Test\.java|Tests\.java)$/i;
25022
+ function isTestFile(file) {
25023
+ return TEST_PATH_RE3.test(file) || TEST_FILENAME_RE.test(file);
25024
+ }
25025
+ var PROVIDER_PATTERNS = [
25026
+ {
25027
+ name: "AWS access key",
25028
+ regex: /\bAKIA[0-9A-Z]{16}\b/,
25029
+ severity: "critical",
25030
+ level: "error",
25031
+ fix: "Rotate the AWS access key immediately and move it to an environment variable or AWS Secrets Manager."
25032
+ },
25033
+ {
25034
+ name: "GitHub personal access token",
25035
+ regex: /\bghp_[A-Za-z0-9]{36}\b/,
25036
+ severity: "critical",
25037
+ level: "error",
25038
+ fix: "Revoke the token at https://github.com/settings/tokens and store secrets in CI/CD secrets, not source."
25039
+ },
25040
+ {
25041
+ name: "GitHub OAuth token",
25042
+ regex: /\bgho_[A-Za-z0-9]{36}\b/,
25043
+ severity: "critical",
25044
+ level: "error",
25045
+ fix: "Revoke the OAuth token and store secrets outside source control."
25046
+ },
25047
+ {
25048
+ name: "GitHub user-to-server token",
25049
+ regex: /\bghu_[A-Za-z0-9]{36}\b/,
25050
+ severity: "critical",
25051
+ level: "error",
25052
+ fix: "Revoke the GitHub user-to-server token and store secrets outside source control."
25053
+ },
25054
+ {
25055
+ name: "GitHub server-to-server token",
25056
+ regex: /\bghs_[A-Za-z0-9]{36}\b/,
25057
+ severity: "critical",
25058
+ level: "error",
25059
+ fix: "Revoke the GitHub server-to-server token and store secrets outside source control."
25060
+ },
25061
+ {
25062
+ name: "GitHub refresh token",
25063
+ regex: /\bghr_[A-Za-z0-9]{36}\b/,
25064
+ severity: "critical",
25065
+ level: "error",
25066
+ fix: "Revoke the GitHub refresh token and store secrets outside source control."
25067
+ },
25068
+ {
25069
+ name: "Stripe live secret key",
25070
+ regex: /\bsk_live_[A-Za-z0-9]{24,}\b/,
25071
+ severity: "critical",
25072
+ level: "error",
25073
+ fix: "Rotate the Stripe secret key in the Stripe Dashboard and load it from a secrets manager."
25074
+ },
25075
+ {
25076
+ name: "Stripe live publishable key",
25077
+ regex: /\bpk_live_[A-Za-z0-9]{24,}\b/,
25078
+ severity: "high",
25079
+ level: "warning",
25080
+ fix: "Publishable keys are not secret but should still not be checked in to back-end source files; verify front-end vs back-end context."
25081
+ },
25082
+ {
25083
+ name: "OpenAI API key",
25084
+ regex: /\bsk-[A-Za-z0-9]{48}\b/,
25085
+ severity: "critical",
25086
+ level: "error",
25087
+ fix: "Revoke the OpenAI key at https://platform.openai.com/api-keys and load from environment."
25088
+ },
25089
+ {
25090
+ name: "Anthropic API key",
25091
+ regex: /\bsk-ant-[A-Za-z0-9_-]{90,}\b/,
25092
+ severity: "critical",
25093
+ level: "error",
25094
+ fix: "Revoke the Anthropic key in the Console and load from environment."
25095
+ },
25096
+ {
25097
+ name: "Slack token",
25098
+ regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/,
25099
+ severity: "critical",
25100
+ level: "error",
25101
+ fix: "Revoke the Slack token and load from environment."
25102
+ },
25103
+ {
25104
+ name: "Google API key",
25105
+ regex: /\bAIza[0-9A-Za-z_-]{35}\b/,
25106
+ severity: "critical",
25107
+ level: "error",
25108
+ fix: "Restrict the Google API key by referrer / IP in the GCP console or revoke it."
25109
+ },
25110
+ {
25111
+ name: "JSON Web Token",
25112
+ regex: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/,
25113
+ severity: "critical",
25114
+ level: "error",
25115
+ fix: "JWTs in source carry whatever scope they were minted with; rotate signing keys and remove the token."
25116
+ },
25117
+ {
25118
+ name: "PEM private key",
25119
+ regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----/,
25120
+ severity: "critical",
25121
+ level: "error",
25122
+ fix: "Remove the private key from source control immediately, rotate the corresponding public key, and store keys outside the repository."
25123
+ },
25124
+ {
25125
+ name: "npm access token",
25126
+ regex: /\bnpm_[A-Za-z0-9]{36}\b/,
25127
+ severity: "critical",
25128
+ level: "error",
25129
+ fix: "Revoke the npm token at https://www.npmjs.com/settings/<user>/tokens and load from environment."
25130
+ }
25131
+ ];
25132
+ var STRING_LITERAL_RE = /(["'`])((?:\\.|(?!\1).){8,200})\1/g;
25133
+ var BASE64ISH_RE = /^[A-Za-z0-9+/=_-]+$/;
25134
+ var HEXISH_RE = /^[a-fA-F0-9]+$/;
25135
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
25136
+ var PLACEHOLDER_RE = /(?:changeme|your[-_]?(?:key|secret|token|password)(?:[-_]?here)?|replace[-_]?me|example[-_]?(?:key|secret|token)?|placeholder|todo|fixme|test[-_]?(?:key|secret|token)|fake[-_]?(?:key|secret|token)|dummy|sample|insert[-_]?your)/i;
25137
+ function isBareHashShape(s) {
25138
+ const n = s.length;
25139
+ if (n !== 32 && n !== 40 && n !== 64) return false;
25140
+ return HEXISH_RE.test(s);
25141
+ }
25142
+ function isAllSameChar(s) {
25143
+ if (s.length < 2) return false;
25144
+ const c = s.charAt(0);
25145
+ for (let i2 = 1; i2 < s.length; i2++) if (s.charAt(i2) !== c) return false;
25146
+ return true;
25147
+ }
25148
+ function tryBase64Decode(s) {
25149
+ if (s.length % 4 !== 0 && !/=+$/.test(s)) return null;
25150
+ try {
25151
+ return globalThis.atob(s);
25152
+ } catch {
25153
+ return null;
25154
+ }
25155
+ }
25156
+ function looksLikeBase64Json(s) {
25157
+ const decoded = tryBase64Decode(s);
25158
+ if (!decoded) return false;
25159
+ const trimmed = decoded.trimStart();
25160
+ return trimmed.startsWith("{") || trimmed.startsWith("[");
25161
+ }
25162
+ function shannonEntropy(s) {
25163
+ const freq = /* @__PURE__ */ new Map();
25164
+ for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
25165
+ const len = s.length;
25166
+ let h = 0;
25167
+ for (const n of freq.values()) {
25168
+ const p = n / len;
25169
+ h -= p * Math.log2(p);
25170
+ }
25171
+ return h;
25172
+ }
25173
+ var CREDENTIAL_NAME_RE = /(?:key|secret|token|password|passwd|credential|api[_-]?key)/i;
25174
+ var TEST_CALL_RE = /\b(?:expect|assert|describe|it|test)\s*\(/;
25175
+ var COMMENT_EXAMPLE_RE = /(?:\/\/|#)\s*(?:example|sample|test|fixture)/i;
25176
+ var ScanSecretsPass = class {
25177
+ name = "scan-secrets";
25178
+ category = "security";
25179
+ run(ctx) {
25180
+ const file = ctx.graph.ir.meta.file;
25181
+ if (isTestFile(file)) {
25182
+ return { providerFindings: 0, entropyFindings: 0 };
25183
+ }
25184
+ const lines = ctx.code.split("\n");
25185
+ const prior = ctx.getFindings?.() ?? [];
25186
+ const seen = /* @__PURE__ */ new Set();
25187
+ for (const f of prior) {
25188
+ if (f.file !== file) continue;
25189
+ if (f.rule_id === "hardcoded-credential" || f.rule_id === "hardcoded-credential-entropy") {
25190
+ seen.add(`${f.line}:${f.rule_id}`);
25191
+ }
25192
+ }
25193
+ let providerFindings = 0;
25194
+ let entropyFindings = 0;
25195
+ for (let i2 = 0; i2 < lines.length; i2++) {
25196
+ const lineText = lines[i2];
25197
+ const lineNum = i2 + 1;
25198
+ for (const pattern of PROVIDER_PATTERNS) {
25199
+ const m = pattern.regex.exec(lineText);
25200
+ if (!m) continue;
25201
+ const key = `${lineNum}:hardcoded-credential`;
25202
+ if (seen.has(key)) continue;
25203
+ seen.add(key);
25204
+ ctx.addFinding({
25205
+ id: `hardcoded-credential-${file}-${lineNum}`,
25206
+ pass: this.name,
25207
+ category: this.category,
25208
+ rule_id: "hardcoded-credential",
25209
+ cwe: "CWE-798",
25210
+ severity: pattern.severity,
25211
+ level: pattern.level,
25212
+ message: `Hardcoded credential: ${pattern.name} detected`,
25213
+ file,
25214
+ line: lineNum,
25215
+ snippet: lineText.trim().substring(0, 120),
25216
+ fix: pattern.fix,
25217
+ evidence: { provider: pattern.name, match: m[0].substring(0, 40) }
25218
+ });
25219
+ providerFindings += 1;
25220
+ break;
25221
+ }
25222
+ }
25223
+ for (let i2 = 0; i2 < lines.length; i2++) {
25224
+ const lineText = lines[i2];
25225
+ const lineNum = i2 + 1;
25226
+ if (TEST_CALL_RE.test(lineText)) continue;
25227
+ if (COMMENT_EXAMPLE_RE.test(lineText)) continue;
25228
+ STRING_LITERAL_RE.lastIndex = 0;
25229
+ let match;
25230
+ while ((match = STRING_LITERAL_RE.exec(lineText)) !== null) {
25231
+ const value = match[2];
25232
+ if (!this.isCandidate(value)) continue;
25233
+ if (!this.passesEntropyGate(value, lineText)) continue;
25234
+ const key = `${lineNum}:hardcoded-credential-entropy`;
25235
+ if (seen.has(key)) continue;
25236
+ if (seen.has(`${lineNum}:hardcoded-credential`)) continue;
25237
+ seen.add(key);
25238
+ ctx.addFinding({
25239
+ id: `hardcoded-credential-entropy-${file}-${lineNum}`,
25240
+ pass: this.name,
25241
+ category: this.category,
25242
+ rule_id: "hardcoded-credential-entropy",
25243
+ cwe: "CWE-798",
25244
+ severity: "high",
25245
+ level: "warning",
25246
+ message: `Possible hardcoded secret: high-entropy string literal (${value.length} chars)`,
25247
+ file,
25248
+ line: lineNum,
25249
+ snippet: lineText.trim().substring(0, 120),
25250
+ fix: "If this is a credential, move it to environment / secrets manager. If it is sample data, add an `example` / `test` marker or disable this pass via `disabledPasses: ['scan-secrets']`.",
25251
+ evidence: { kind: "entropy", length: value.length }
25252
+ });
25253
+ entropyFindings += 1;
25254
+ }
25255
+ }
25256
+ return { providerFindings, entropyFindings };
25257
+ }
25258
+ /** Length + shape + denylist filter before entropy is computed. */
25259
+ isCandidate(s) {
25260
+ if (s.length < 20 || s.length > 200) return false;
25261
+ if (!BASE64ISH_RE.test(s) && !HEXISH_RE.test(s)) return false;
25262
+ if (UUID_RE.test(s)) return false;
25263
+ if (isBareHashShape(s)) return false;
25264
+ if (isAllSameChar(s)) return false;
25265
+ if (PLACEHOLDER_RE.test(s)) return false;
25266
+ if (looksLikeBase64Json(s)) return false;
25267
+ return true;
25268
+ }
25269
+ /**
25270
+ * Shannon-entropy gate. Base64-shaped strings need higher entropy than
25271
+ * hex-shaped (hex alphabet is 4 bits/char by construction). When the
25272
+ * surrounding line contains a credential-shaped variable name, both
25273
+ * thresholds drop by 0.2 bits/char.
25274
+ */
25275
+ passesEntropyGate(value, lineText) {
25276
+ const isHex = HEXISH_RE.test(value);
25277
+ const boost = CREDENTIAL_NAME_RE.test(lineText) ? 0.2 : 0;
25278
+ const threshold = isHex ? 3.5 - boost : 4.3 - boost;
25279
+ const h = shannonEntropy(value);
25280
+ return h >= threshold;
25281
+ }
25282
+ };
25283
+
24994
25284
  // src/analysis/metrics/passes/size-metrics-pass.ts
24995
25285
  var SizeMetricsPass = class {
24996
25286
  name = "size-metrics";
@@ -25835,6 +26125,7 @@ async function analyze(code, filePath, language, options = {}) {
25835
26125
  pipeline.add(new SinkFilterPass());
25836
26126
  pipeline.add(new TaintPropagationPass());
25837
26127
  pipeline.add(new InterproceduralPass());
26128
+ if (!disabledPasses.has("scan-secrets")) pipeline.add(new ScanSecretsPass());
25838
26129
  if (!disabledPasses.has("dead-code")) pipeline.add(new DeadCodePass());
25839
26130
  if (!disabledPasses.has("missing-await")) pipeline.add(new MissingAwaitPass());
25840
26131
  if (!disabledPasses.has("n-plus-one")) pipeline.add(new NPlusOnePass());
@@ -4110,8 +4110,7 @@ async function loadLanguage(language, wasmPath) {
4110
4110
  if (loading) {
4111
4111
  return loading;
4112
4112
  }
4113
- const grammarName = language === "typescript" ? "javascript" : language;
4114
- const wasmModule = configuredLanguageModules[language] ?? configuredLanguageModules[grammarName];
4113
+ const wasmModule = configuredLanguageModules[language];
4115
4114
  if (wasmModule) {
4116
4115
  const loadPromise2 = (async () => {
4117
4116
  const lang = await Language.load(wasmModule);
@@ -4210,20 +4209,19 @@ async function getDefaultWasmPath() {
4210
4209
  return "node_modules/web-tree-sitter/web-tree-sitter.wasm";
4211
4210
  }
4212
4211
  async function getDefaultLanguagePath(language) {
4213
- const grammarName = language === "typescript" ? "javascript" : language;
4214
4212
  const mods = await getNodeModules();
4215
4213
  if (mods && moduleDir) {
4216
4214
  const packageRoot = mods.join(moduleDir, "..", "..");
4217
- const distWasmPath = mods.join(packageRoot, "dist", "wasm", `tree-sitter-${grammarName}.wasm`);
4215
+ const distWasmPath = mods.join(packageRoot, "dist", "wasm", `tree-sitter-${language}.wasm`);
4218
4216
  if (mods.existsSync(distWasmPath)) {
4219
4217
  return distWasmPath;
4220
4218
  }
4221
- const packageWasmPath = mods.join(packageRoot, "wasm", `tree-sitter-${grammarName}.wasm`);
4219
+ const packageWasmPath = mods.join(packageRoot, "wasm", `tree-sitter-${language}.wasm`);
4222
4220
  if (mods.existsSync(packageWasmPath)) {
4223
4221
  return packageWasmPath;
4224
4222
  }
4225
4223
  }
4226
- return `wasm/tree-sitter-${grammarName}.wasm`;
4224
+ return `wasm/tree-sitter-${language}.wasm`;
4227
4225
  }
4228
4226
  function isInitialized() {
4229
4227
  return parserInitialized;
@@ -4932,6 +4930,30 @@ function extractJSParameters(params) {
4932
4930
  annotations: [],
4933
4931
  line: child.startPosition.row + 1
4934
4932
  });
4933
+ } else if (child.type === "required_parameter" || child.type === "optional_parameter") {
4934
+ const patternNode = child.childForFieldName("pattern");
4935
+ if (!patternNode) continue;
4936
+ let paramName;
4937
+ if (patternNode.type === "identifier") {
4938
+ paramName = getNodeText(patternNode);
4939
+ } else if (patternNode.type === "rest_pattern" || patternNode.type === "rest_element") {
4940
+ const inner = patternNode.namedChildCount > 0 ? patternNode.namedChild(0) : null;
4941
+ if (!inner) continue;
4942
+ paramName = "..." + getNodeText(inner);
4943
+ } else {
4944
+ paramName = getNodeText(patternNode);
4945
+ }
4946
+ const typeNode = child.childForFieldName("type");
4947
+ let paramType = null;
4948
+ if (typeNode) {
4949
+ paramType = getNodeText(typeNode).replace(/^:\s*/, "");
4950
+ }
4951
+ parameters.push({
4952
+ name: paramName,
4953
+ type: paramType,
4954
+ annotations: [],
4955
+ line: child.startPosition.row + 1
4956
+ });
4935
4957
  }
4936
4958
  }
4937
4959
  return parameters;
@@ -4045,8 +4045,7 @@ async function loadLanguage(language, wasmPath) {
4045
4045
  if (loading) {
4046
4046
  return loading;
4047
4047
  }
4048
- const grammarName = language === "typescript" ? "javascript" : language;
4049
- const wasmModule = configuredLanguageModules[language] ?? configuredLanguageModules[grammarName];
4048
+ const wasmModule = configuredLanguageModules[language];
4050
4049
  if (wasmModule) {
4051
4050
  const loadPromise2 = (async () => {
4052
4051
  const lang = await Language.load(wasmModule);
@@ -4145,20 +4144,19 @@ async function getDefaultWasmPath() {
4145
4144
  return "node_modules/web-tree-sitter/web-tree-sitter.wasm";
4146
4145
  }
4147
4146
  async function getDefaultLanguagePath(language) {
4148
- const grammarName = language === "typescript" ? "javascript" : language;
4149
4147
  const mods = await getNodeModules();
4150
4148
  if (mods && moduleDir) {
4151
4149
  const packageRoot = mods.join(moduleDir, "..", "..");
4152
- const distWasmPath = mods.join(packageRoot, "dist", "wasm", `tree-sitter-${grammarName}.wasm`);
4150
+ const distWasmPath = mods.join(packageRoot, "dist", "wasm", `tree-sitter-${language}.wasm`);
4153
4151
  if (mods.existsSync(distWasmPath)) {
4154
4152
  return distWasmPath;
4155
4153
  }
4156
- const packageWasmPath = mods.join(packageRoot, "wasm", `tree-sitter-${grammarName}.wasm`);
4154
+ const packageWasmPath = mods.join(packageRoot, "wasm", `tree-sitter-${language}.wasm`);
4157
4155
  if (mods.existsSync(packageWasmPath)) {
4158
4156
  return packageWasmPath;
4159
4157
  }
4160
4158
  }
4161
- return `wasm/tree-sitter-${grammarName}.wasm`;
4159
+ return `wasm/tree-sitter-${language}.wasm`;
4162
4160
  }
4163
4161
  function isInitialized() {
4164
4162
  return parserInitialized;
@@ -4867,6 +4865,30 @@ function extractJSParameters(params) {
4867
4865
  annotations: [],
4868
4866
  line: child.startPosition.row + 1
4869
4867
  });
4868
+ } else if (child.type === "required_parameter" || child.type === "optional_parameter") {
4869
+ const patternNode = child.childForFieldName("pattern");
4870
+ if (!patternNode) continue;
4871
+ let paramName;
4872
+ if (patternNode.type === "identifier") {
4873
+ paramName = getNodeText(patternNode);
4874
+ } else if (patternNode.type === "rest_pattern" || patternNode.type === "rest_element") {
4875
+ const inner = patternNode.namedChildCount > 0 ? patternNode.namedChild(0) : null;
4876
+ if (!inner) continue;
4877
+ paramName = "..." + getNodeText(inner);
4878
+ } else {
4879
+ paramName = getNodeText(patternNode);
4880
+ }
4881
+ const typeNode = child.childForFieldName("type");
4882
+ let paramType = null;
4883
+ if (typeNode) {
4884
+ paramType = getNodeText(typeNode).replace(/^:\s*/, "");
4885
+ }
4886
+ parameters.push({
4887
+ name: paramName,
4888
+ type: paramType,
4889
+ annotations: [],
4890
+ line: child.startPosition.row + 1
4891
+ });
4870
4892
  }
4871
4893
  }
4872
4894
  return parameters;
@@ -751,6 +751,38 @@ function extractJSParameters(params) {
751
751
  line: child.startPosition.row + 1,
752
752
  });
753
753
  }
754
+ else if (child.type === 'required_parameter' || child.type === 'optional_parameter') {
755
+ // TypeScript-grammar parameter: pattern (identifier or destructuring) + optional type_annotation
756
+ const patternNode = child.childForFieldName('pattern');
757
+ if (!patternNode)
758
+ continue;
759
+ let paramName;
760
+ if (patternNode.type === 'identifier') {
761
+ paramName = getNodeText(patternNode);
762
+ }
763
+ else if (patternNode.type === 'rest_pattern' || patternNode.type === 'rest_element') {
764
+ const inner = patternNode.namedChildCount > 0 ? patternNode.namedChild(0) : null;
765
+ if (!inner)
766
+ continue;
767
+ paramName = '...' + getNodeText(inner);
768
+ }
769
+ else {
770
+ // object_pattern, array_pattern, or assignment_pattern with default
771
+ paramName = getNodeText(patternNode);
772
+ }
773
+ const typeNode = child.childForFieldName('type');
774
+ let paramType = null;
775
+ if (typeNode) {
776
+ // type_annotation includes the leading ':'; strip it for storage parity with other languages
777
+ paramType = getNodeText(typeNode).replace(/^:\s*/, '');
778
+ }
779
+ parameters.push({
780
+ name: paramName,
781
+ type: paramType,
782
+ annotations: [],
783
+ line: child.startPosition.row + 1,
784
+ });
785
+ }
754
786
  }
755
787
  return parameters;
756
788
  }