Skip to content

Commit

Permalink
More specific TemplateStringsArray type for tagged templates
Browse files Browse the repository at this point in the history
  • Loading branch information
rbuckton committed Jun 16, 2022
1 parent 3fc5f96 commit 70fdb52
Show file tree
Hide file tree
Showing 35 changed files with 567 additions and 104 deletions.
110 changes: 92 additions & 18 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,7 @@ namespace ts {
let deferredGlobalAsyncIterableIteratorType: GenericType | undefined;
let deferredGlobalAsyncGeneratorType: GenericType | undefined;
let deferredGlobalTemplateStringsArrayType: ObjectType | undefined;
let deferredGlobalTemplateStringsArrayOfSymbol: Symbol | undefined;
let deferredGlobalImportMetaType: ObjectType;
let deferredGlobalImportMetaExpressionType: ObjectType;
let deferredGlobalImportCallOptionsType: ObjectType | undefined;
Expand Down Expand Up @@ -14102,6 +14103,11 @@ namespace ts {
return (deferredGlobalBigIntType ||= getGlobalType("BigInt" as __String, /*arity*/ 0, /*reportErrors*/ false)) || emptyObjectType;
}

function getGlobalTemplateStringsArrayOfSymbol(): Symbol | undefined {
deferredGlobalTemplateStringsArrayOfSymbol ||= getGlobalTypeAliasSymbol("TemplateStringsArrayOf" as __String, /*arity*/ 2, /*reportErrors*/ true) || unknownSymbol;
return deferredGlobalTemplateStringsArrayOfSymbol === unknownSymbol ? undefined : deferredGlobalTemplateStringsArrayOfSymbol;
}

/**
* Instantiates a global type that is generic with some element type, and returns that instantiation.
*/
Expand Down Expand Up @@ -21171,6 +21177,43 @@ namespace ts {
return isArrayType(type) || !(type.flags & TypeFlags.Nullable) && isTypeAssignableTo(type, anyReadonlyArrayType);
}

/**
* Returns `type` if it is an array or tuple type. If `type` is an intersection type,
* returns the rightmost constituent that is an array or tuple type, but only if there are no
* other constituents to that contain properties that overlap with array- or tuple- specific
* members (i.e., index signatures, numeric string property names, or `length`).
*/
function tryGetNonShadowedArrayOrTupleType(type: Type) {
if (isArrayOrTupleType(type)) {
return type;
}

if (!(type.flags & TypeFlags.Intersection)) {
return undefined;
}

let arrayOrTupleConstituent: TypeReference | undefined;
for (const constituent of (type as IntersectionType).types) {
if (isArrayOrTupleType(constituent)) {
arrayOrTupleConstituent = constituent;
}
else {
const properties = getPropertiesOfType(constituent);
for (const property of properties) {
if (isNumericLiteralName(property.escapedName) || property.escapedName === "length" as __String) {
return undefined;
}
}

if (some(getIndexInfosOfType(constituent))) {
return undefined;
}
}
}

return arrayOrTupleConstituent;
}

function getSingleBaseForNonAugmentingSubtype(type: Type) {
if (!(getObjectFlags(type) & ObjectFlags.Reference) || !(getObjectFlags((type as TypeReference).target) & ObjectFlags.ClassOrInterface)) {
return undefined;
Expand Down Expand Up @@ -22830,68 +22873,69 @@ namespace ts {
}
// Infer from the members of source and target only if the two types are possibly related
if (!typesDefinitelyUnrelated(source, target)) {
if (isArrayOrTupleType(source)) {
const sourceArrayOrTuple = tryGetNonShadowedArrayOrTupleType(source);
if (sourceArrayOrTuple) {
if (isTupleType(target)) {
const sourceArity = getTypeReferenceArity(source);
const sourceArity = getTypeReferenceArity(sourceArrayOrTuple);
const targetArity = getTypeReferenceArity(target);
const elementTypes = getTypeArguments(target);
const elementFlags = target.target.elementFlags;
// When source and target are tuple types with the same structure (fixed, variadic, and rest are matched
// to the same kind in each position), simply infer between the element types.
if (isTupleType(source) && isTupleTypeStructureMatching(source, target)) {
if (isTupleType(sourceArrayOrTuple) && isTupleTypeStructureMatching(sourceArrayOrTuple, target)) {
for (let i = 0; i < targetArity; i++) {
inferFromTypes(getTypeArguments(source)[i], elementTypes[i]);
inferFromTypes(getTypeArguments(sourceArrayOrTuple)[i], elementTypes[i]);
}
return;
}
const startLength = isTupleType(source) ? Math.min(source.target.fixedLength, target.target.fixedLength) : 0;
const endLength = Math.min(isTupleType(source) ? getEndElementCount(source.target, ElementFlags.Fixed) : 0,
const startLength = isTupleType(sourceArrayOrTuple) ? Math.min(sourceArrayOrTuple.target.fixedLength, target.target.fixedLength) : 0;
const endLength = Math.min(isTupleType(sourceArrayOrTuple) ? getEndElementCount(sourceArrayOrTuple.target, ElementFlags.Fixed) : 0,
target.target.hasRestElement ? getEndElementCount(target.target, ElementFlags.Fixed) : 0);
// Infer between starting fixed elements.
for (let i = 0; i < startLength; i++) {
inferFromTypes(getTypeArguments(source)[i], elementTypes[i]);
inferFromTypes(getTypeArguments(sourceArrayOrTuple)[i], elementTypes[i]);
}
if (!isTupleType(source) || sourceArity - startLength - endLength === 1 && source.target.elementFlags[startLength] & ElementFlags.Rest) {
if (!isTupleType(sourceArrayOrTuple) || sourceArity - startLength - endLength === 1 && sourceArrayOrTuple.target.elementFlags[startLength] & ElementFlags.Rest) {
// Single rest element remains in source, infer from that to every element in target
const restType = getTypeArguments(source)[startLength];
const restType = getTypeArguments(sourceArrayOrTuple)[startLength];
for (let i = startLength; i < targetArity - endLength; i++) {
inferFromTypes(elementFlags[i] & ElementFlags.Variadic ? createArrayType(restType) : restType, elementTypes[i]);
}
}
else {
const middleLength = targetArity - startLength - endLength;
if (middleLength === 2 && elementFlags[startLength] & elementFlags[startLength + 1] & ElementFlags.Variadic && isTupleType(source)) {
if (middleLength === 2 && elementFlags[startLength] & elementFlags[startLength + 1] & ElementFlags.Variadic && isTupleType(sourceArrayOrTuple)) {
// Middle of target is [...T, ...U] and source is tuple type
const targetInfo = getInferenceInfoForType(elementTypes[startLength]);
if (targetInfo && targetInfo.impliedArity !== undefined) {
// Infer slices from source based on implied arity of T.
inferFromTypes(sliceTupleType(source, startLength, endLength + sourceArity - targetInfo.impliedArity), elementTypes[startLength]);
inferFromTypes(sliceTupleType(source, startLength + targetInfo.impliedArity, endLength), elementTypes[startLength + 1]);
inferFromTypes(sliceTupleType(sourceArrayOrTuple, startLength, endLength + sourceArity - targetInfo.impliedArity), elementTypes[startLength]);
inferFromTypes(sliceTupleType(sourceArrayOrTuple, startLength + targetInfo.impliedArity, endLength), elementTypes[startLength + 1]);
}
}
else if (middleLength === 1 && elementFlags[startLength] & ElementFlags.Variadic) {
// Middle of target is exactly one variadic element. Infer the slice between the fixed parts in the source.
// If target ends in optional element(s), make a lower priority a speculative inference.
const endsInOptional = target.target.elementFlags[targetArity - 1] & ElementFlags.Optional;
const sourceSlice = isTupleType(source) ? sliceTupleType(source, startLength, endLength) : createArrayType(getTypeArguments(source)[0]);
const sourceSlice = isTupleType(sourceArrayOrTuple) ? sliceTupleType(sourceArrayOrTuple, startLength, endLength) : createArrayType(getTypeArguments(sourceArrayOrTuple)[0]);
inferWithPriority(sourceSlice, elementTypes[startLength], endsInOptional ? InferencePriority.SpeculativeTuple : 0);
}
else if (middleLength === 1 && elementFlags[startLength] & ElementFlags.Rest) {
// Middle of target is exactly one rest element. If middle of source is not empty, infer union of middle element types.
const restType = isTupleType(source) ? getElementTypeOfSliceOfTupleType(source, startLength, endLength) : getTypeArguments(source)[0];
const restType = isTupleType(sourceArrayOrTuple) ? getElementTypeOfSliceOfTupleType(sourceArrayOrTuple, startLength, endLength) : getTypeArguments(sourceArrayOrTuple)[0];
if (restType) {
inferFromTypes(restType, elementTypes[startLength]);
}
}
}
// Infer between ending fixed elements
for (let i = 0; i < endLength; i++) {
inferFromTypes(getTypeArguments(source)[sourceArity - i - 1], elementTypes[targetArity - i - 1]);
inferFromTypes(getTypeArguments(sourceArrayOrTuple)[sourceArity - i - 1], elementTypes[targetArity - i - 1]);
}
return;
}
if (isArrayType(target)) {
inferFromIndexTypes(source, target);
inferFromIndexTypes(sourceArrayOrTuple, target);
return;
}
}
Expand Down Expand Up @@ -27548,7 +27592,37 @@ namespace ts {
return checkIteratedTypeOrElementType(IterationUse.Spread, arrayOrIterableType, undefinedType, node.expression);
}

function getTemplateStringsArrayOf(cookedTypes: Type[], rawTypes: Type[]) {
const templateStringsArrayOfAlias = getGlobalTemplateStringsArrayOfSymbol();
if (!templateStringsArrayOfAlias) return getGlobalTemplateStringsArrayType();
const cookedType = createTupleType(cookedTypes, /*elementFlags*/ undefined, /*readonly*/ true);
const rawType = createTupleType(rawTypes, /*elementFlags*/ undefined, /*readonly*/ true);
return getTypeAliasInstantiation(templateStringsArrayOfAlias, [cookedType, rawType]);
}

function getRawLiteralType(node: TemplateLiteralLikeNode) {
const text = getRawTextOfTemplateLiteralLike(node, getSourceFileOfNode(node));
return getStringLiteralType(text);
}

function checkSyntheticExpression(node: SyntheticExpression): Type {
if (isTemplateLiteral(node.parent) && node.type === getGlobalTemplateStringsArrayType()) {
const cookedStrings: Type[] = [];
const rawStrings: Type[] = [];
if (isNoSubstitutionTemplateLiteral(node.parent)) {
cookedStrings.push(getStringLiteralType(node.parent.text));
rawStrings.push(getRawLiteralType(node.parent));
}
else {
cookedStrings.push(getStringLiteralType(node.parent.head.text));
rawStrings.push(getRawLiteralType(node.parent.head));
for (const templateSpan of node.parent.templateSpans) {
cookedStrings.push(getStringLiteralType(templateSpan.literal.text));
rawStrings.push(getRawLiteralType(templateSpan.literal));
}
}
return getTemplateStringsArrayOf(cookedStrings, rawStrings);
}
return node.isSpread ? getIndexedAccessType(node.type, numberType) : node.type;
}

Expand Down Expand Up @@ -30587,10 +30661,10 @@ namespace ts {
let typeArguments: NodeArray<TypeNode> | undefined;

if (!isDecorator) {
typeArguments = (node as CallExpression).typeArguments;
typeArguments = node.typeArguments;

// We already perform checking on the type arguments on the class declaration itself.
if (isTaggedTemplate || isJsxOpeningOrSelfClosingElement || (node as CallExpression).expression.kind !== SyntaxKind.SuperKeyword) {
if (isTaggedTemplate || isJsxOpeningOrSelfClosingElement || node.expression.kind !== SyntaxKind.SuperKeyword) {
forEach(typeArguments, checkSourceElement);
}
}
Expand Down
22 changes: 1 addition & 21 deletions src/compiler/transformers/taggedTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,27 +76,7 @@ namespace ts {
* @param node The ES6 template literal.
*/
function getRawLiteral(node: TemplateLiteralLikeNode, currentSourceFile: SourceFile) {
// Find original source text, since we need to emit the raw strings of the tagged template.
// The raw strings contain the (escaped) strings of what the user wrote.
// Examples: `\n` is converted to "\\n", a template string with a newline to "\n".
let text = node.rawText;
if (text === undefined) {
Debug.assertIsDefined(currentSourceFile,
"Template literal node is missing 'rawText' and does not have a source file. Possibly bad transform.");
text = getSourceTextOfNodeFromSourceFile(currentSourceFile, node);

// text contains the original source, it will also contain quotes ("`"), dolar signs and braces ("${" and "}"),
// thus we need to remove those characters.
// First template piece starts with "`", others with "}"
// Last template piece ends with "`", others with "${"
const isLast = node.kind === SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === SyntaxKind.TemplateTail;
text = text.substring(1, text.length - (isLast ? 1 : 2));
}

// Newline normalization:
// ES6 Spec 11.8.6.1 - Static Semantics of TV's and TRV's
// <CR><LF> and <CR> LineTerminatorSequences are normalized to <LF> for both TV and TRV.
text = text.replace(/\r\n?/g, "\n");
const text = getRawTextOfTemplateLiteralLike(node, currentSourceFile);
return setTextRange(factory.createStringLiteral(text), node);
}
}
24 changes: 24 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,30 @@ namespace ts {
return Debug.fail(`Literal kind '${node.kind}' not accounted for.`);
}

export function getRawTextOfTemplateLiteralLike(node: TemplateLiteralLikeNode, sourceFile: SourceFile) {
// Find original source text, since we need to emit the raw strings of the tagged template.
// The raw strings contain the (escaped) strings of what the user wrote.
// Examples: `\n` is converted to "\\n", a template string with a newline to "\n".
let text = node.rawText;
if (text === undefined) {
Debug.assertIsDefined(sourceFile,
"Template literal node is missing 'rawText' and does not have a source file. Possibly bad transform.");
text = getSourceTextOfNodeFromSourceFile(sourceFile, node);

// text contains the original source, it will also contain quotes ("`"), dolar signs and braces ("${" and "}"),
// thus we need to remove those characters.
// First template piece starts with "`", others with "}"
// Last template piece ends with "`", others with "${"
const isLast = node.kind === SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === SyntaxKind.TemplateTail;
text = text.substring(1, text.length - (isLast ? 1 : 2));
}

// Newline normalization:
// ES6 Spec 11.8.6.1 - Static Semantics of TV's and TRV's
// <CR><LF> and <CR> LineTerminatorSequences are normalized to <LF> for both TV and TRV.
return text.replace(/\r\n?/g, "\n");
}

function canUseOriginalText(node: LiteralLikeNode, flags: GetLiteralTextFlags): boolean {
if (nodeIsSynthesized(node) || !node.parent || (flags & GetLiteralTextFlags.TerminateUnterminatedLiterals && node.isUnterminated)) {
return false;
Expand Down
1 change: 1 addition & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,7 @@ namespace FourSlashInterface {
varEntry("Number"),
interfaceEntry("NumberConstructor"),
interfaceEntry("TemplateStringsArray"),
typeEntry("TemplateStringsArrayOf"),
interfaceEntry("ImportMeta"),
interfaceEntry("ImportCallOptions"),
interfaceEntry("ImportAssertions"),
Expand Down
2 changes: 2 additions & 0 deletions src/lib/es5.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,8 @@ interface TemplateStringsArray extends ReadonlyArray<string> {
readonly raw: readonly string[];
}

type TemplateStringsArrayOf<Cooked extends readonly string[], Raw extends readonly string[] = Cooked> = Cooked & { readonly raw: Raw };

/**
* The type of `import.meta`.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ tests/cases/conformance/es6/destructuring/destructuringParameterDeclaration4.ts(
a1(...array2); // Error parameter type is (number|string)[]
~~~~~~
!!! error TS2552: Cannot find name 'array2'. Did you mean 'Array'?
!!! related TS2728 /.ts/lib.es5.d.ts:1470:13: 'Array' is declared here.
!!! related TS2728 /.ts/lib.es5.d.ts:1472:13: 'Array' is declared here.
a5([1, 2, "string", false, true]); // Error, parameter type is [any, any, [[any]]]
~~~~~~~~
!!! error TS2322: Type 'string' is not assignable to type '[[any]]'.
Expand Down
Loading

0 comments on commit 70fdb52

Please sign in to comment.