Skip to content

Commit fafd048

Browse files
committed
Ensure parameters are in scope when converting parameter/return types to type nodes
1 parent 132638b commit fafd048

13 files changed

+124
-163
lines changed

src/compiler/checker.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7276,6 +7276,82 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
72767276
}
72777277

72787278
const expandedParams = getExpandedParameters(signature, /*skipUnionExpanding*/ true)[0];
7279+
7280+
// For regular function/method declarations, the enclosing declaration will already be signature.declaration,
7281+
// so this is a no-op, but for arrow functions and function expressions, the enclosing declaration will be
7282+
// the declaration that the arrow function / function expression is assigned to.
7283+
//
7284+
// If the parameters or return type include "typeof globalThis.paramName", using the wrong scope will lead
7285+
// us to believe that we can emit "typeof paramName" instead, even though that would refer to the parameter,
7286+
// not the global. Make sure we are in the right scope by changing the enclosingDeclaration to the function.
7287+
//
7288+
// We can't use the declaration directly; it may be in another file and so we may lose access to symbols
7289+
// accessible to the current enclosing declaration, or gain access to symbols not accessible to the current
7290+
// enclosing declaration. To keep this chain accurate, insert a fake scope into the chain which makes the
7291+
// function's parameters visible.
7292+
//
7293+
// If the declaration is in a JS file, then we don't need to do this at all, as there are no annotations besides
7294+
// JSDoc, which are always outside the function declaration, so are not in the parameter scope.
7295+
let cleanup: (() => void) | undefined;
7296+
if (
7297+
context.enclosingDeclaration
7298+
&& signature.declaration
7299+
&& signature.declaration !== context.enclosingDeclaration
7300+
&& !isInJSFile(signature.declaration)
7301+
&& some(expandedParams)
7302+
) {
7303+
// As a performance optimization, share the symbol table between all faked scopes
7304+
// in this node builder chain. This is especially needed when we are working on an
7305+
// excessively deep type; if we don't do this, then we spend all of our time adding
7306+
// more and more scopes that need to be searched in isSymbolAccessible later. Since
7307+
// all we really want to do is to mark certain names as unavailable, we can just
7308+
// keep all of the names we're introducing in one large table and push/pop from it as
7309+
// needed; isSymbolAccessible will walk upward and find the closest "fake" scope,
7310+
// which will conveniently report on any and all faked scopes in the chain.
7311+
//
7312+
// It'd likely be better to store this somewhere else for isSymbolAccessible, but
7313+
// since that API _only_ uses the enclosing declaration (and its parents), this is
7314+
// seems like the best way to inject names into that search process.
7315+
const existingFakeScope = findAncestor(context.enclosingDeclaration, node => !!getNodeLinks(node).fakeScopeForSignatureDeclaration);
7316+
Debug.assertOptionalNode(existingFakeScope, isBlock);
7317+
7318+
const locals = existingFakeScope?.locals ?? createSymbolTable();
7319+
7320+
let newLocals: __String[] | undefined;
7321+
for (const param of expandedParams) {
7322+
if (!locals.has(param.escapedName)) {
7323+
newLocals = append(newLocals, param.escapedName);
7324+
locals.set(param.escapedName, param);
7325+
}
7326+
}
7327+
7328+
if (newLocals) {
7329+
function removeNewLocals() {
7330+
forEach(newLocals, s => locals.delete(s));
7331+
}
7332+
7333+
if (existingFakeScope) {
7334+
cleanup = removeNewLocals;
7335+
}
7336+
else {
7337+
// Use a Block for this; the type of the node doesn't matter so long as it
7338+
// has locals, and this is cheaper/easier than using a function-ish Node.
7339+
const fakeScope = parseNodeFactory.createBlock(emptyArray);
7340+
getNodeLinks(fakeScope).fakeScopeForSignatureDeclaration = true;
7341+
fakeScope.locals = locals;
7342+
7343+
const saveEnclosingDeclaration = context.enclosingDeclaration;
7344+
setParent(fakeScope, saveEnclosingDeclaration);
7345+
context.enclosingDeclaration = fakeScope;
7346+
7347+
cleanup = () => {
7348+
context.enclosingDeclaration = saveEnclosingDeclaration;
7349+
removeNewLocals();
7350+
};
7351+
}
7352+
}
7353+
}
7354+
72797355
// If the expanded parameter list had a variadic in a non-trailing position, don't expand it
72807356
const parameters = (some(expandedParams, p => p !== expandedParams[expandedParams.length - 1] && !!(getCheckFlags(p) & CheckFlags.RestParameter)) ? signature.parameters : expandedParams).map(parameter => symbolToParameterDeclaration(parameter, context, kind === SyntaxKind.Constructor, options?.privateSymbolVisitor, options?.bundledImports));
72817357
const thisParameter = context.flags & NodeBuilderFlags.OmitThisParameter ? undefined : tryGetThisParameterDeclaration(signature, context);
@@ -7331,6 +7407,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
73317407
node.typeArguments = factory.createNodeArray(typeArguments);
73327408
}
73337409

7410+
cleanup?.();
73347411
return node;
73357412
}
73367413

@@ -7996,13 +8073,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
79968073
return !(getObjectFlags(type) & ObjectFlags.Reference) || !isTypeReferenceNode(existing) || length(existing.typeArguments) >= getMinTypeArgumentCount((type as TypeReference).target.typeParameters);
79978074
}
79988075

8076+
function getEnclosingDeclarationIgnoringFakeScope(enclosingDeclaration: Node) {
8077+
return findAncestor(enclosingDeclaration, n => !getNodeLinks(n).fakeScopeForSignatureDeclaration);
8078+
}
8079+
79998080
/**
80008081
* Unlike `typeToTypeNodeHelper`, this handles setting up the `AllowUniqueESSymbolType` flag
80018082
* so a `unique symbol` is returned when appropriate for the input symbol, rather than `typeof sym`
80028083
*/
80038084
function serializeTypeForDeclaration(context: NodeBuilderContext, type: Type, symbol: Symbol, enclosingDeclaration: Node | undefined, includePrivateSymbol?: (s: Symbol) => void, bundled?: boolean) {
80048085
if (!isErrorType(type) && enclosingDeclaration) {
8005-
const declWithExistingAnnotation = getDeclarationWithTypeAnnotation(symbol, enclosingDeclaration);
8086+
const declWithExistingAnnotation = getDeclarationWithTypeAnnotation(symbol, getEnclosingDeclarationIgnoringFakeScope(enclosingDeclaration));
80068087
if (declWithExistingAnnotation && !isFunctionLikeDeclaration(declWithExistingAnnotation) && !isGetAccessorDeclaration(declWithExistingAnnotation)) {
80078088
// try to reuse the existing annotation
80088089
const existing = getEffectiveTypeAnnotationNode(declWithExistingAnnotation)!;
@@ -8038,7 +8119,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
80388119
function serializeReturnTypeForSignature(context: NodeBuilderContext, type: Type, signature: Signature, includePrivateSymbol?: (s: Symbol) => void, bundled?: boolean) {
80398120
if (!isErrorType(type) && context.enclosingDeclaration) {
80408121
const annotation = signature.declaration && getEffectiveReturnTypeNode(signature.declaration);
8041-
if (!!findAncestor(annotation, n => n === context.enclosingDeclaration) && annotation) {
8122+
const enclosingDeclarationIgnoringFakeScope = getEnclosingDeclarationIgnoringFakeScope(context.enclosingDeclaration);
8123+
if (!!findAncestor(annotation, n => n === enclosingDeclarationIgnoringFakeScope) && annotation) {
80428124
const annotated = getTypeFromTypeNode(annotation);
80438125
const thisInstantiated = annotated.flags & TypeFlags.TypeParameter && (annotated as TypeParameter).isThisType ? instantiateType(annotated, signature.mapper) : annotated;
80448126
if (thisInstantiated === type && existingTypeNodeIsNotReferenceOrIsReferenceWithCompatibleTypeArgumentCount(annotation, type)) {

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5997,6 +5997,7 @@ export interface NodeLinks {
59975997
serializedTypes?: Map<string, SerializedTypeEntry>; // Collection of types serialized at this location
59985998
decoratorSignature?: Signature; // Signature for decorator as if invoked by the runtime.
59995999
parameterInitializerContainsUndefined?: boolean; // True if this is a parameter declaration whose type annotation contains "undefined".
6000+
fakeScopeForSignatureDeclaration?: boolean; // True if this is a fake scope injected into an enclosing declaration chain.
60006001
}
60016002

60026003
/** @internal */

0 commit comments

Comments
 (0)