carrick 0.3.111 → 0.3.112
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/bin/carrick.mjs +3 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +5 -0
- package/dist/contract.js.map +1 -1
- package/dist/hook/post-edit.js +11 -2
- package/dist/hook/post-edit.js.map +1 -1
- package/dist/hook/post-read.d.ts +2 -0
- package/dist/hook/post-read.js +89 -0
- package/dist/hook/post-read.js.map +1 -0
- package/dist/hook/reuse.d.ts +24 -0
- package/dist/hook/reuse.js +58 -2
- package/dist/hook/reuse.js.map +1 -1
- package/dist/init/settings.js +6 -0
- package/dist/init/settings.js.map +1 -1
- package/dist/render.d.ts +22 -2
- package/dist/render.js +53 -2
- package/dist/render.js.map +1 -1
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/hooks/hooks.json +10 -0
- package/sidecar/dist/src/capture/check-probe.d.ts +8 -0
- package/sidecar/dist/src/capture/check-probe.js +9 -1
- package/sidecar/dist/src/capture/self-check.d.ts +20 -1
- package/sidecar/dist/src/capture/self-check.js +30 -11
- package/sidecar/dist/src/type-inferrer.d.ts +61 -1
- package/sidecar/dist/src/type-inferrer.js +299 -23
|
@@ -257,6 +257,83 @@ function typeText(type, enclosingNode) {
|
|
|
257
257
|
* `text` is `string`, alone or beside `null` and `undefined` in either order:
|
|
258
258
|
* the only payload a raw-text read publishes (carrick#1842).
|
|
259
259
|
*/
|
|
260
|
+
/**
|
|
261
|
+
* Source text with every plain string literal written in double quotes:
|
|
262
|
+
* `'/a'`, `"/a"` and an interpolation-free `` `/a` `` are one value, so text
|
|
263
|
+
* that differs only in its quotes compares equal (carrick#2056).
|
|
264
|
+
*
|
|
265
|
+
* The text is read left to right, pairing quotes the way the scanner does,
|
|
266
|
+
* so a template's closing backtick never pairs with the next template's
|
|
267
|
+
* opening one. A literal holding a quote, a backslash or an interpolation is
|
|
268
|
+
* copied as it is, and so is an unterminated one with everything after it.
|
|
269
|
+
*/
|
|
270
|
+
export function canonicalStringQuotes(text) {
|
|
271
|
+
const isQuote = (ch) => ch === "'" || ch === '"' || ch === '`';
|
|
272
|
+
// The index just past the literal opening at `start`, or -1.
|
|
273
|
+
const literalEnd = (start) => {
|
|
274
|
+
const quote = text[start];
|
|
275
|
+
let j = start + 1;
|
|
276
|
+
while (j < text.length) {
|
|
277
|
+
const ch = text[j];
|
|
278
|
+
if (ch === '\\') {
|
|
279
|
+
j += 2;
|
|
280
|
+
}
|
|
281
|
+
else if (ch === quote) {
|
|
282
|
+
return j + 1;
|
|
283
|
+
}
|
|
284
|
+
else if (quote === '`' && ch === '$' && text[j + 1] === '{') {
|
|
285
|
+
const close = codeEnd(j + 2);
|
|
286
|
+
if (close < 0)
|
|
287
|
+
return -1;
|
|
288
|
+
j = close + 1;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
j++;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return -1;
|
|
295
|
+
};
|
|
296
|
+
// The index of the `}` that closes an interpolation whose code starts at `start`, or -1.
|
|
297
|
+
const codeEnd = (start) => {
|
|
298
|
+
let depth = 0;
|
|
299
|
+
let j = start;
|
|
300
|
+
while (j < text.length) {
|
|
301
|
+
const ch = text[j];
|
|
302
|
+
if (isQuote(ch)) {
|
|
303
|
+
const end = literalEnd(j);
|
|
304
|
+
if (end < 0)
|
|
305
|
+
return -1;
|
|
306
|
+
j = end;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (ch === '{')
|
|
310
|
+
depth++;
|
|
311
|
+
if (ch === '}') {
|
|
312
|
+
if (depth === 0)
|
|
313
|
+
return j;
|
|
314
|
+
depth--;
|
|
315
|
+
}
|
|
316
|
+
j++;
|
|
317
|
+
}
|
|
318
|
+
return -1;
|
|
319
|
+
};
|
|
320
|
+
let out = '';
|
|
321
|
+
let i = 0;
|
|
322
|
+
while (i < text.length) {
|
|
323
|
+
if (!isQuote(text[i])) {
|
|
324
|
+
out += text[i];
|
|
325
|
+
i++;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const end = literalEnd(i);
|
|
329
|
+
if (end < 0)
|
|
330
|
+
return out + text.slice(i);
|
|
331
|
+
const body = text.slice(i + 1, end - 1);
|
|
332
|
+
out += /['"`\\]|\$\{/.test(body) ? text.slice(i, end) : `"${body}"`;
|
|
333
|
+
i = end;
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
260
337
|
function isBareStringText(text) {
|
|
261
338
|
const members = text.split('|').map((member) => member.trim());
|
|
262
339
|
return (members.includes('string') &&
|
|
@@ -1516,8 +1593,12 @@ export class TypeInferrer {
|
|
|
1516
1593
|
}
|
|
1517
1594
|
inferCallResult(sourceFile, request, extractionConfig) {
|
|
1518
1595
|
const callExpr = this.resolveTargetCallExpression(sourceFile, request);
|
|
1596
|
+
// A call's result is read off the call. A locator that finds no call names
|
|
1597
|
+
// nothing this can read, and whatever other node its text overlaps is not
|
|
1598
|
+
// the call's result (carrick#2056); the retype abstains on the same miss.
|
|
1519
1599
|
if (!callExpr) {
|
|
1520
|
-
|
|
1600
|
+
this.log(`Call-result locator at ${request.file_path}:${request.line_number} finds no call; leaving unresolved`);
|
|
1601
|
+
return null;
|
|
1521
1602
|
}
|
|
1522
1603
|
// Walk up from the already-found call expression instead of re-searching
|
|
1523
1604
|
const func = this.findContainingFunctionForNode(callExpr);
|
|
@@ -1928,18 +2009,25 @@ export class TypeInferrer {
|
|
|
1928
2009
|
parent.getInitializer() === current) {
|
|
1929
2010
|
break;
|
|
1930
2011
|
}
|
|
1931
|
-
if (!
|
|
1932
|
-
!Node.isAwaitExpression(parent) &&
|
|
1933
|
-
!Node.isNonNullExpression(parent) &&
|
|
1934
|
-
!Node.isAsExpression(parent) &&
|
|
1935
|
-
!Node.isTypeAssertion(parent) &&
|
|
1936
|
-
!Node.isSatisfiesExpression(parent)) {
|
|
2012
|
+
if (!this.wrapsReadTransparently(parent)) {
|
|
1937
2013
|
return undefined;
|
|
1938
2014
|
}
|
|
1939
2015
|
current = parent;
|
|
1940
2016
|
}
|
|
1941
2017
|
return typeNode;
|
|
1942
2018
|
}
|
|
2019
|
+
/**
|
|
2020
|
+
* A wrapper between a body read and the binding it initialises that leaves
|
|
2021
|
+
* the read's value as it is: `await`, parentheses, `!`, a cast, `satisfies`.
|
|
2022
|
+
*/
|
|
2023
|
+
wrapsReadTransparently(node) {
|
|
2024
|
+
return (Node.isParenthesizedExpression(node) ||
|
|
2025
|
+
Node.isAwaitExpression(node) ||
|
|
2026
|
+
Node.isNonNullExpression(node) ||
|
|
2027
|
+
Node.isAsExpression(node) ||
|
|
2028
|
+
Node.isTypeAssertion(node) ||
|
|
2029
|
+
Node.isSatisfiesExpression(node));
|
|
2030
|
+
}
|
|
1943
2031
|
/**
|
|
1944
2032
|
* A stated type with `any` or `unknown` written anywhere in it (`unknown`,
|
|
1945
2033
|
* `Record<string, unknown>`, `{ items: any[] }`) leaves a position open: it
|
|
@@ -2183,6 +2271,24 @@ export class TypeInferrer {
|
|
|
2183
2271
|
'to one body field by a keyed decorator; publishing the body those fields make up');
|
|
2184
2272
|
return this.declaredRequestInferredType(request, keyedBody, this.getNodeLocation(located));
|
|
2185
2273
|
}
|
|
2274
|
+
// carrick#2057: the locator named a member of the body the handler parsed
|
|
2275
|
+
// (`body.items` after `const body = (await request.json()) as ImportBody`,
|
|
2276
|
+
// or a binding destructured from that read). A member is one field of the
|
|
2277
|
+
// body, so publishing its type states the whole body is that field. The
|
|
2278
|
+
// body is what the source states at the read; where it states nothing,
|
|
2279
|
+
// nothing is published.
|
|
2280
|
+
const bodyRead = this.parsedBodyReadHolding(located);
|
|
2281
|
+
if (bodyRead) {
|
|
2282
|
+
const stated = this.requestStatedAtBodyRead(request, bodyRead);
|
|
2283
|
+
if (stated) {
|
|
2284
|
+
this.log(`Request locator at ${request.file_path}:${request.line_number} names a member of the ` +
|
|
2285
|
+
'body its handler parses; publishing the body the source states at that read');
|
|
2286
|
+
return stated;
|
|
2287
|
+
}
|
|
2288
|
+
this.log(`Request locator at ${request.file_path}:${request.line_number} names a member of the ` +
|
|
2289
|
+
'body its handler parses, and the source states no type at that read; leaving unresolved');
|
|
2290
|
+
return null;
|
|
2291
|
+
}
|
|
2186
2292
|
// Mirror inferCallResult: strip `await`/`as`/parens/`!` so the inner
|
|
2187
2293
|
// expression's type (not the surrounding `Promise<any>`) is read.
|
|
2188
2294
|
let unwrapped = this.unwrapExpressionNode(located);
|
|
@@ -4681,6 +4787,25 @@ export class TypeInferrer {
|
|
|
4681
4787
|
}
|
|
4682
4788
|
return [expression];
|
|
4683
4789
|
}
|
|
4790
|
+
/**
|
|
4791
|
+
* The symbol an anchor is named from: the type's own symbol, unless that
|
|
4792
|
+
* symbol's name is synthetic (`__type`, `__object`, ...), in which case the
|
|
4793
|
+
* alias symbol (carrick#2148).
|
|
4794
|
+
*
|
|
4795
|
+
* `type Widget = { id: string }` and an instantiated `Page<Order>` carry
|
|
4796
|
+
* the anonymous `__type` as their own symbol and the alias as their alias
|
|
4797
|
+
* symbol. Reading the own symbol first and stopping because it is truthy
|
|
4798
|
+
* left every object-literal alias without an anchor, while the same shape
|
|
4799
|
+
* declared as an interface had one. The own symbol still wins whenever it
|
|
4800
|
+
* has a real name, so every type anchored before keeps its anchor.
|
|
4801
|
+
*/
|
|
4802
|
+
anchorSymbolOf(type) {
|
|
4803
|
+
const own = type.getSymbol();
|
|
4804
|
+
if (own && !own.getName().startsWith('__')) {
|
|
4805
|
+
return own;
|
|
4806
|
+
}
|
|
4807
|
+
return type.getAliasSymbol() ?? own;
|
|
4808
|
+
}
|
|
4684
4809
|
/**
|
|
4685
4810
|
* The deterministic source symbol of a resolved type (`Payment` for a payload
|
|
4686
4811
|
* typed `Payment`), or `undefined` when there is no single user-defined
|
|
@@ -4691,7 +4816,7 @@ export class TypeInferrer {
|
|
|
4691
4816
|
* `primary_type_symbol` so the manifest anchor no longer depends on the LLM.
|
|
4692
4817
|
*/
|
|
4693
4818
|
primaryTypeSymbol(type) {
|
|
4694
|
-
const name =
|
|
4819
|
+
const name = this.anchorSymbolOf(type)?.getName();
|
|
4695
4820
|
// Reject the compiler's synthetic names for anonymous shapes (`__type`,
|
|
4696
4821
|
// `__object`, `__function`, …). They are not user-facing type names, so
|
|
4697
4822
|
// they would be a meaningless — and non-resolvable — anchor.
|
|
@@ -4711,7 +4836,8 @@ export class TypeInferrer {
|
|
|
4711
4836
|
* declarations IN its `source_file`, so the request must point at the file
|
|
4712
4837
|
* that actually declares the tsc-witnessed payload type.
|
|
4713
4838
|
*
|
|
4714
|
-
* The declarations read are those of the
|
|
4839
|
+
* The declarations read are those of the symbol the anchor is named from
|
|
4840
|
+
* (`anchorSymbolOf`: the type's own symbol, or its alias's), so a name
|
|
4715
4841
|
* imported through a barrel reports the file that declares it, and a name
|
|
4716
4842
|
* two files declare reports the one this type resolves to. A declaration in
|
|
4717
4843
|
* an installed package or a TypeScript lib is reported as it is found, as
|
|
@@ -4730,7 +4856,7 @@ export class TypeInferrer {
|
|
|
4730
4856
|
if (this.primaryTypeSymbol(type) === undefined) {
|
|
4731
4857
|
return undefined;
|
|
4732
4858
|
}
|
|
4733
|
-
const decls =
|
|
4859
|
+
const decls = this.anchorSymbolOf(type)?.getDeclarations() ?? [];
|
|
4734
4860
|
const declaring = decls.find((d) => Node.isInterfaceDeclaration(d) ||
|
|
4735
4861
|
Node.isTypeAliasDeclaration(d) ||
|
|
4736
4862
|
Node.isClassDeclaration(d) ||
|
|
@@ -5388,6 +5514,56 @@ export class TypeInferrer {
|
|
|
5388
5514
|
}
|
|
5389
5515
|
return undefined;
|
|
5390
5516
|
}
|
|
5517
|
+
/**
|
|
5518
|
+
* The platform body read whose result `located` is a MEMBER of, or
|
|
5519
|
+
* `undefined` (carrick#2057).
|
|
5520
|
+
*
|
|
5521
|
+
* A member is a property or element access off a binding, or a name
|
|
5522
|
+
* destructured out of one. The binding must be initialised by the body read
|
|
5523
|
+
* `platformBodyReadIn` finds in the function that declares it, with only
|
|
5524
|
+
* transparent wrappers between the two (`await`, parentheses, `!`, a cast,
|
|
5525
|
+
* `satisfies`). The binding itself is not a member, and neither is a member
|
|
5526
|
+
* of anything else (a query object, a parameter, an outbound response).
|
|
5527
|
+
*/
|
|
5528
|
+
parsedBodyReadHolding(located) {
|
|
5529
|
+
let node = this.unwrapExpressionNode(located);
|
|
5530
|
+
let member = false;
|
|
5531
|
+
while (Node.isPropertyAccessExpression(node) ||
|
|
5532
|
+
Node.isElementAccessExpression(node) ||
|
|
5533
|
+
Node.isNonNullExpression(node) ||
|
|
5534
|
+
Node.isParenthesizedExpression(node)) {
|
|
5535
|
+
if (Node.isPropertyAccessExpression(node) || Node.isElementAccessExpression(node)) {
|
|
5536
|
+
member = true;
|
|
5537
|
+
}
|
|
5538
|
+
node = node.getExpression();
|
|
5539
|
+
}
|
|
5540
|
+
if (!Node.isIdentifier(node))
|
|
5541
|
+
return undefined;
|
|
5542
|
+
let declaration = node.getSymbol()?.getDeclarations()?.[0];
|
|
5543
|
+
while (declaration && Node.isBindingElement(declaration)) {
|
|
5544
|
+
member = true;
|
|
5545
|
+
declaration = declaration.getParent()?.getParent();
|
|
5546
|
+
}
|
|
5547
|
+
if (!member || !declaration || !Node.isVariableDeclaration(declaration))
|
|
5548
|
+
return undefined;
|
|
5549
|
+
const handler = this.findContainingFunctionForNode(declaration);
|
|
5550
|
+
const read = handler ? this.platformBodyReadIn(handler) : undefined;
|
|
5551
|
+
if (!read)
|
|
5552
|
+
return undefined;
|
|
5553
|
+
let current = read;
|
|
5554
|
+
for (;;) {
|
|
5555
|
+
const parent = current.getParent();
|
|
5556
|
+
if (!parent)
|
|
5557
|
+
return undefined;
|
|
5558
|
+
if (parent === declaration) {
|
|
5559
|
+
return declaration.getInitializer() === current ? read : undefined;
|
|
5560
|
+
}
|
|
5561
|
+
if (!this.wrapsReadTransparently(parent)) {
|
|
5562
|
+
return undefined;
|
|
5563
|
+
}
|
|
5564
|
+
current = parent;
|
|
5565
|
+
}
|
|
5566
|
+
}
|
|
5391
5567
|
/**
|
|
5392
5568
|
* The request contract the source states AT a body read, or null when it
|
|
5393
5569
|
* states none there (carrick#807).
|
|
@@ -6785,21 +6961,119 @@ export class TypeInferrer {
|
|
|
6785
6961
|
// locator whose exact match failed would bind to the smallest embedded
|
|
6786
6962
|
// sub-expression (pickBestMatch prefers small nodes), e.g. the 8-char
|
|
6787
6963
|
// literal "active" inside a res.json object payload (#335).
|
|
6964
|
+
//
|
|
6965
|
+
// A node the text itself places strictly inside what it names is a
|
|
6966
|
+
// fragment, however much of the text it covers (carrick#2056): the
|
|
6967
|
+
// `new URLSearchParams(…)` inside a call's URL argument, or the object a
|
|
6968
|
+
// callback nested in the named object returns. Only the node the text
|
|
6969
|
+
// names, or a wrapper the text adds around it, may be bound this way. A
|
|
6970
|
+
// fragment still points at what the text names where the source agrees
|
|
6971
|
+
// with the text up to it: the enclosing node of the named kind whose text
|
|
6972
|
+
// opens with the text's own opening up to the fragment (`new Response(`
|
|
6973
|
+
// before a payload, when the text dropped the call's second argument).
|
|
6788
6974
|
const MIN_REVERSE_MATCH_LEN = 8;
|
|
6789
6975
|
const MIN_REVERSE_MATCH_COVERAGE = 0.5;
|
|
6790
|
-
const
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6976
|
+
const containingMatches = candidates.filter((c) => c.text.includes(normalizedTarget));
|
|
6977
|
+
if (containingMatches.length > 0) {
|
|
6978
|
+
return this.pickBestMatch(containingMatches.map((c) => c.node), lineNumber);
|
|
6979
|
+
}
|
|
6980
|
+
const reverseMatches = candidates.filter((c) => c.text.length >= MIN_REVERSE_MATCH_LEN &&
|
|
6981
|
+
c.text.length >= normalizedTarget.length * MIN_REVERSE_MATCH_COVERAGE &&
|
|
6982
|
+
normalizedTarget.includes(c.text));
|
|
6983
|
+
if (reverseMatches.length === 0)
|
|
6984
|
+
return undefined;
|
|
6985
|
+
const named = this.locatorNames(normalizedTarget);
|
|
6986
|
+
if (!named) {
|
|
6987
|
+
return this.pickBestMatch(reverseMatches.map((c) => c.node), lineNumber);
|
|
6988
|
+
}
|
|
6989
|
+
const textOf = new Map(candidates.map((c) => [c.node, c.text]));
|
|
6990
|
+
const bound = new Set();
|
|
6991
|
+
for (const c of reverseMatches) {
|
|
6992
|
+
if (named.texts.has(c.text)) {
|
|
6993
|
+
bound.add(c.node);
|
|
6994
|
+
continue;
|
|
6995
|
+
}
|
|
6996
|
+
const at = normalizedTarget.indexOf(c.text, named.rootStart + 1);
|
|
6997
|
+
if (at < 0)
|
|
6998
|
+
continue;
|
|
6999
|
+
const opening = normalizedTarget.slice(named.rootStart, at);
|
|
7000
|
+
const enclosing = c.node.getFirstAncestor((ancestor) => ancestor.getKind() === named.rootKind &&
|
|
7001
|
+
textOf.get(ancestor)?.startsWith(opening) === true);
|
|
7002
|
+
if (enclosing)
|
|
7003
|
+
bound.add(enclosing);
|
|
6801
7004
|
}
|
|
6802
|
-
return undefined;
|
|
7005
|
+
return bound.size > 0 ? this.pickBestMatch([...bound], lineNumber) : undefined;
|
|
7006
|
+
}
|
|
7007
|
+
/**
|
|
7008
|
+
* What a locator's text names, read off its own parse (carrick#2056):
|
|
7009
|
+
*
|
|
7010
|
+
* - `texts`: the normalized text of the expression it names and of each
|
|
7011
|
+
* wrapper it carries around it (`return`, a `const x =` binding, a label,
|
|
7012
|
+
* `await`, parentheses, a cast, `satisfies`, `!`). A node strictly inside
|
|
7013
|
+
* that expression is not among them;
|
|
7014
|
+
* - `rootKind` and `rootStart`: the innermost of those, its kind, and where
|
|
7015
|
+
* its text starts in the locator.
|
|
7016
|
+
*
|
|
7017
|
+
* `null` when the text does not parse as one expression or one statement:
|
|
7018
|
+
* there is no structure to read, and the caller keeps its text rule.
|
|
7019
|
+
*/
|
|
7020
|
+
locatorNames(normalizedTarget) {
|
|
7021
|
+
const parsed = (source) => {
|
|
7022
|
+
const file = ts.createSourceFile('locator.ts', source, ts.ScriptTarget.Latest, true);
|
|
7023
|
+
const diagnostics = file
|
|
7024
|
+
.parseDiagnostics;
|
|
7025
|
+
return diagnostics && diagnostics.length === 0 && file.statements.length === 1 ? file : null;
|
|
7026
|
+
};
|
|
7027
|
+
// An object literal is a block as a statement, so try the text as an
|
|
7028
|
+
// expression first.
|
|
7029
|
+
const EXPRESSION_OPENING = '(\n';
|
|
7030
|
+
const asExpression = parsed(`${EXPRESSION_OPENING}${normalizedTarget}\n);`);
|
|
7031
|
+
let node;
|
|
7032
|
+
let file = asExpression;
|
|
7033
|
+
let offset = 0;
|
|
7034
|
+
if (asExpression) {
|
|
7035
|
+
const statement = asExpression.statements[0];
|
|
7036
|
+
if (ts.isExpressionStatement(statement) && ts.isParenthesizedExpression(statement.expression)) {
|
|
7037
|
+
node = statement.expression.expression;
|
|
7038
|
+
offset = EXPRESSION_OPENING.length;
|
|
7039
|
+
}
|
|
7040
|
+
}
|
|
7041
|
+
else {
|
|
7042
|
+
file = parsed(normalizedTarget);
|
|
7043
|
+
node = file?.statements[0];
|
|
7044
|
+
}
|
|
7045
|
+
if (!file || !node)
|
|
7046
|
+
return null;
|
|
7047
|
+
const texts = new Set();
|
|
7048
|
+
let root;
|
|
7049
|
+
while (node) {
|
|
7050
|
+
if (ts.isLabeledStatement(node)) {
|
|
7051
|
+
node = node.statement;
|
|
7052
|
+
}
|
|
7053
|
+
else if (ts.isExpressionStatement(node) || ts.isReturnStatement(node)) {
|
|
7054
|
+
node = node.expression;
|
|
7055
|
+
}
|
|
7056
|
+
else if (ts.isVariableStatement(node)) {
|
|
7057
|
+
const declarations = node.declarationList.declarations;
|
|
7058
|
+
node = declarations.length === 1 ? declarations[0].initializer : undefined;
|
|
7059
|
+
}
|
|
7060
|
+
else {
|
|
7061
|
+
texts.add(this.normalizeWhitespace(node.getText(file)));
|
|
7062
|
+
root = node;
|
|
7063
|
+
node =
|
|
7064
|
+
ts.isParenthesizedExpression(node) ||
|
|
7065
|
+
ts.isAwaitExpression(node) ||
|
|
7066
|
+
ts.isAsExpression(node) ||
|
|
7067
|
+
ts.isSatisfiesExpression(node) ||
|
|
7068
|
+
ts.isNonNullExpression(node) ||
|
|
7069
|
+
ts.isTypeAssertionExpression(node)
|
|
7070
|
+
? node.expression
|
|
7071
|
+
: undefined;
|
|
7072
|
+
}
|
|
7073
|
+
}
|
|
7074
|
+
if (!root)
|
|
7075
|
+
return null;
|
|
7076
|
+
return { texts, rootKind: root.kind, rootStart: root.getStart(file) - offset };
|
|
6803
7077
|
}
|
|
6804
7078
|
/**
|
|
6805
7079
|
* Pick the best match from a set of candidate nodes:
|
|
@@ -6840,7 +7114,7 @@ export class TypeInferrer {
|
|
|
6840
7114
|
* for the same reason.
|
|
6841
7115
|
*/
|
|
6842
7116
|
normalizeWhitespace(text) {
|
|
6843
|
-
|
|
7117
|
+
const collapsed = text
|
|
6844
7118
|
.replace(/\s+/g, ' ')
|
|
6845
7119
|
// A member chain broken before its dot (`client\n .list(…)`) reads the
|
|
6846
7120
|
// same as `client.list(…)`; a space around `.` / `?.` means nothing
|
|
@@ -6849,6 +7123,8 @@ export class TypeInferrer {
|
|
|
6849
7123
|
.replace(/\s*,?\s*([}\)\]])/g, '$1')
|
|
6850
7124
|
.replace(/([\(\[{])\s+/g, '$1')
|
|
6851
7125
|
.trim();
|
|
7126
|
+
// A string literal reads the same in any quotes (carrick#2056).
|
|
7127
|
+
return canonicalStringQuotes(collapsed);
|
|
6852
7128
|
}
|
|
6853
7129
|
/**
|
|
6854
7130
|
* Format a human-readable location string for error messages.
|