Skip to content

Commit

Permalink
Add globalThis (#29332)
Browse files Browse the repository at this point in the history
* Restore original code from bind-toplevel-this

With one or two additional comments

* Working in JS, but the symbol is not right.

Still need to

1. Make it work in Typescript.
2. Add test (and make them work) for the other uses of GlobalThis:
window, globalThis, etc.

* Check in TS also; update some tests

Lots of tests still fail, but all but 1 change so far has been correct.

* Update baselines

A couple of tests still fail and need to be fixed.

* Handle type references to globalThis

The type reference must be `typeof globalThis`. Just `globalThis` will
be treated as a value reference in type position -- an error.

* Restore former behaviour of implicitThis errors

I left the noImplicitThis rule for captured use of global this in an
arrow function, even though technically it isn't `any` any more --
it's typeof globalThis.  However, you should still use some other method
to access globals inside an arrow, because captured-global-this is super
confusing there.

* Test values with type globalThis

I ran into a problem with intersecting `Window & typeof globalThis`:

1. This adds a new index signature to Window, which is probably not
desired. In fact, with noImplicitAny, it's not desired on globalThis
either I think.
2. Adding this type requires editing TSJS-lib-generator, not this repo.

So I added the test cases and will probably update them later, when
those two problems are fixed.

* Add esnext declaration for globalThis

* Switch to symbol-based approach

I decided I didn't like the import-type-based approach.

Update baselines to reflect the difference.

* Do not suggest globals for completions at toplevel

* Add tests of element and property access

* Look up globalThis using normal resolution

globalThis is no longer constructed lazily. Its synthetic Identifier
node is also now more realistic.

* Update fourslash tests

* Add missed fourslash test update

* Remove esnext.globalthis.d.ts too

* Add chained globalThis self-lookup test

* Attempt at making globalThis readonly

In progress, had to interrupt for other work.

* Add/update tests

* Addres PR comments:

1. Add parameter to tryGetThisTypeAt to exclude globalThis.
2. Use combined Module flag instead combining them in-place.
3. SymbolDisplay doesn't print 'module globalThis' for this expressions
anymore.
  • Loading branch information
sandersn authored Feb 27, 2019
1 parent 13c08ab commit be2db9d
Show file tree
Hide file tree
Showing 167 changed files with 1,372 additions and 400 deletions.
9 changes: 7 additions & 2 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2499,8 +2499,13 @@ namespace ts {
declareSymbol(symbolTable, containingClass.symbol, node, SymbolFlags.Property, SymbolFlags.None, /*isReplaceableByMethod*/ true);
break;
case SyntaxKind.SourceFile:
// this.foo assignment in a source file
// Do not bind. It would be nice to support this someday though.
// this.property = assignment in a source file -- declare symbol in exports for a module, in locals for a script
if ((thisContainer as SourceFile).commonJsModuleIndicator) {
declareSymbol(thisContainer.symbol.exports!, thisContainer.symbol, node, SymbolFlags.Property | SymbolFlags.ExportValue, SymbolFlags.None);
}
else {
declareSymbolAndAddToSymbolTable(node, SymbolFlags.FunctionScopedVariable, SymbolFlags.FunctionScopedVariableExcludes);
}
break;

default:
Expand Down
59 changes: 42 additions & 17 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,16 @@ namespace ts {
const emitResolver = createResolver();
const nodeBuilder = createNodeBuilder();

const globals = createSymbolTable();
const undefinedSymbol = createSymbol(SymbolFlags.Property, "undefined" as __String);
undefinedSymbol.declarations = [];

const globalThisSymbol = createSymbol(SymbolFlags.Module, "globalThis" as __String, CheckFlags.Readonly);
globalThisSymbol.exports = globals;
globalThisSymbol.valueDeclaration = createNode(SyntaxKind.Identifier) as Identifier;
(globalThisSymbol.valueDeclaration as Identifier).escapedText = "globalThis" as __String;
globals.set(globalThisSymbol.escapedName, globalThisSymbol);

const argumentsSymbol = createSymbol(SymbolFlags.Property, "arguments" as __String);
const requireSymbol = createSymbol(SymbolFlags.Property, "require" as __String);

Expand Down Expand Up @@ -310,9 +318,9 @@ namespace ts {
getAccessibleSymbolChain,
getTypePredicateOfSignature: getTypePredicateOfSignature as (signature: Signature) => TypePredicate, // TODO: GH#18217
resolveExternalModuleSymbol,
tryGetThisTypeAt: node => {
tryGetThisTypeAt: (node, includeGlobalThis) => {
node = getParseTreeNode(node);
return node && tryGetThisTypeAt(node);
return node && tryGetThisTypeAt(node, includeGlobalThis);
},
getTypeArgumentConstraint: nodeIn => {
const node = getParseTreeNode(nodeIn, isTypeNode);
Expand Down Expand Up @@ -459,7 +467,6 @@ namespace ts {

const enumNumberIndexInfo = createIndexInfo(stringType, /*isReadonly*/ true);

const globals = createSymbolTable();
interface DuplicateInfoForSymbol {
readonly firstFileLocations: Node[];
readonly secondFileLocations: Node[];
Expand Down Expand Up @@ -9703,7 +9710,7 @@ namespace ts {
}

function getLiteralTypeFromProperties(type: Type, include: TypeFlags) {
return getUnionType(map(getPropertiesOfType(type), t => getLiteralTypeFromProperty(t, include)));
return getUnionType(map(getPropertiesOfType(type), p => getLiteralTypeFromProperty(p, include)));
}

function getNonEnumNumberIndexInfo(type: Type) {
Expand Down Expand Up @@ -16990,25 +16997,27 @@ namespace ts {
captureLexicalThis(node, container);
}

const type = tryGetThisTypeAt(node, container);
if (!type && noImplicitThis) {
// With noImplicitThis, functions may not reference 'this' if it has type 'any'
const diag = error(
node,
capturedByArrowFunction && container.kind === SyntaxKind.SourceFile ?
Diagnostics.The_containing_arrow_function_captures_the_global_value_of_this_which_implicitly_has_type_any :
Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation);
if (!isSourceFile(container)) {
const outsideThis = tryGetThisTypeAt(container);
if (outsideThis) {
addRelatedInfo(diag, createDiagnosticForNode(container, Diagnostics.An_outer_value_of_this_is_shadowed_by_this_container));
const type = tryGetThisTypeAt(node, /*includeGlobalThis*/ true, container);
if (noImplicitThis) {
const globalThisType = getTypeOfSymbol(globalThisSymbol);
if (type === globalThisType && capturedByArrowFunction) {
error(node, Diagnostics.The_containing_arrow_function_captures_the_global_value_of_this);
}
else if (!type) {
// With noImplicitThis, functions may not reference 'this' if it has type 'any'
const diag = error(node, Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation);
if (!isSourceFile(container)) {
const outsideThis = tryGetThisTypeAt(container);
if (outsideThis && outsideThis !== globalThisType) {
addRelatedInfo(diag, createDiagnosticForNode(container, Diagnostics.An_outer_value_of_this_is_shadowed_by_this_container));
}
}
}
}
return type || anyType;
}

function tryGetThisTypeAt(node: Node, container = getThisContainer(node, /*includeArrowFunctions*/ false)): Type | undefined {
function tryGetThisTypeAt(node: Node, includeGlobalThis = true, container = getThisContainer(node, /*includeArrowFunctions*/ false)): Type | undefined {
const isInJS = isInJSFile(node);
if (isFunctionLike(container) &&
(!isInParameterInitializerBeforeContainingFunction(node) || getThisParameter(container))) {
Expand Down Expand Up @@ -17055,6 +17064,16 @@ namespace ts {
return getFlowTypeOfReference(node, type);
}
}
if (isSourceFile(container)) {
// look up in the source file's locals or exports
if (container.commonJsModuleIndicator) {
const fileSymbol = getSymbolOfNode(container);
return fileSymbol && getTypeOfSymbol(fileSymbol);
}
else if (includeGlobalThis) {
return getTypeOfSymbol(globalThisSymbol);
}
}
}

function getClassNameFromPrototypeMethod(container: Node) {
Expand Down Expand Up @@ -19352,6 +19371,12 @@ namespace ts {
if (isJSLiteralType(leftType)) {
return anyType;
}
if (leftType.symbol === globalThisSymbol) {
if (noImplicitAny) {
error(right, Diagnostics.Element_implicitly_has_an_any_type_because_type_0_has_no_index_signature, typeToString(leftType));
}
return anyType;
}
if (right.escapedText && !checkAndReportErrorForExtendingInterface(node)) {
reportNonexistentProperty(right, leftType.flags & TypeFlags.TypeParameter && (leftType as TypeParameter).isThisType ? apparentType : leftType);
}
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -4149,7 +4149,7 @@
"category": "Error",
"code": 7040
},
"The containing arrow function captures the global value of 'this' which implicitly has type 'any'.": {
"The containing arrow function captures the global value of 'this'.": {
"category": "Error",
"code": 7041
},
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3223,7 +3223,7 @@ namespace ts {
*/
/* @internal */ resolveExternalModuleSymbol(symbol: Symbol): Symbol;
/** @param node A location where we might consider accessing `this`. Not necessarily a ThisExpression. */
/* @internal */ tryGetThisTypeAt(node: Node): Type | undefined;
/* @internal */ tryGetThisTypeAt(node: Node, includeGlobalThis?: boolean): Type | undefined;
/* @internal */ getTypeArgumentConstraint(node: TypeNode): Type | undefined;

/**
Expand Down
18 changes: 13 additions & 5 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@ namespace FourSlash {
if ("exact" in options) {
ts.Debug.assert(!("includes" in options) && !("excludes" in options));
if (options.exact === undefined) throw this.raiseError("Expected no completions");
this.verifyCompletionsAreExactly(actualCompletions.entries, toArray(options.exact));
this.verifyCompletionsAreExactly(actualCompletions.entries, toArray(options.exact), options.marker);
}
else {
if (options.includes) {
Expand Down Expand Up @@ -841,14 +841,14 @@ namespace FourSlash {
}
}

private verifyCompletionsAreExactly(actual: ReadonlyArray<ts.CompletionEntry>, expected: ReadonlyArray<FourSlashInterface.ExpectedCompletionEntry>) {
private verifyCompletionsAreExactly(actual: ReadonlyArray<ts.CompletionEntry>, expected: ReadonlyArray<FourSlashInterface.ExpectedCompletionEntry>, marker?: ArrayOrSingle<string | Marker>) {
// First pass: test that names are right. Then we'll test details.
assert.deepEqual(actual.map(a => a.name), expected.map(e => typeof e === "string" ? e : e.name));
assert.deepEqual(actual.map(a => a.name), expected.map(e => typeof e === "string" ? e : e.name), marker ? "At marker " + JSON.stringify(marker) : undefined);

ts.zipWith(actual, expected, (completion, expectedCompletion, index) => {
const name = typeof expectedCompletion === "string" ? expectedCompletion : expectedCompletion.name;
if (completion.name !== name) {
this.raiseError(`Expected completion at index ${index} to be ${name}, got ${completion.name}`);
this.raiseError(`${marker ? JSON.stringify(marker) : "" } Expected completion at index ${index} to be ${name}, got ${completion.name}`);
}
this.verifyCompletionEntry(completion, expectedCompletion);
});
Expand Down Expand Up @@ -4545,6 +4545,7 @@ namespace FourSlashInterface {

export function globalTypesPlus(plus: ReadonlyArray<ExpectedCompletionEntry>): ReadonlyArray<ExpectedCompletionEntry> {
return [
{ name: "globalThis", kind: "module" },
...globalTypeDecls,
...plus,
...typeKeywords,
Expand Down Expand Up @@ -4786,6 +4787,7 @@ namespace FourSlashInterface {
export const globalsInsideFunction = (plus: ReadonlyArray<ExpectedCompletionEntry>): ReadonlyArray<ExpectedCompletionEntry> => [
{ name: "arguments", kind: "local var" },
...plus,
{ name: "globalThis", kind: "module" },
...globalsVars,
{ name: "undefined", kind: "var" },
...globalKeywordsInsideFunction,
Expand Down Expand Up @@ -4921,13 +4923,19 @@ namespace FourSlashInterface {
})();

export const globals: ReadonlyArray<ExpectedCompletionEntryObject> = [
{ name: "globalThis", kind: "module" },
...globalsVars,
{ name: "undefined", kind: "var" },
...globalKeywords
];

export function globalsPlus(plus: ReadonlyArray<ExpectedCompletionEntry>): ReadonlyArray<ExpectedCompletionEntry> {
return [...globalsVars, ...plus, { name: "undefined", kind: "var" }, ...globalKeywords];
return [
{ name: "globalThis", kind: "module" },
...globalsVars,
...plus,
{ name: "undefined", kind: "var" },
...globalKeywords];
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,7 @@ namespace ts.Completions {

// Need to insert 'this.' before properties of `this` type, so only do that if `includeInsertTextCompletions`
if (preferences.includeCompletionsWithInsertText && scopeNode.kind !== SyntaxKind.SourceFile) {
const thisType = typeChecker.tryGetThisTypeAt(scopeNode);
const thisType = typeChecker.tryGetThisTypeAt(scopeNode, /*includeGlobalThis*/ false);
if (thisType) {
for (const symbol of getPropertiesForCompletion(thisType, typeChecker)) {
symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.ThisType };
Expand Down
2 changes: 1 addition & 1 deletion src/services/symbolDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ namespace ts.SymbolDisplay {
displayParts.push(spacePart());
addFullSymbolName(symbol);
}
if (symbolFlags & SymbolFlags.Module) {
if (symbolFlags & SymbolFlags.Module && !isThisExpression) {
prefixNextMeaning();
const declaration = getDeclarationOfKind<ModuleDeclaration>(symbol, SyntaxKind.ModuleDeclaration);
const isNamespace = declaration && declaration.name && declaration.name.kind === SyntaxKind.Identifier;
Expand Down
3 changes: 2 additions & 1 deletion src/testRunner/unittests/tsserver/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,8 @@ namespace ts.projectSystem {
// Check identifiers defined in HTML content are available in .ts file
const project = configuredProjectAt(projectService, 0);
let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, emptyOptions);
assert(completions && completions.entries[0].name === "hello", `expected entry hello to be in completion list`);
assert(completions && completions.entries[1].name === "hello", `expected entry hello to be in completion list`);
assert(completions && completions.entries[0].name === "globalThis", `first entry should be globalThis (not strictly relevant for this test).`);

// Close HTML file
projectService.applyChangesInOpenFiles(
Expand Down
2 changes: 2 additions & 0 deletions tests/baselines/reference/assignmentLHSIsValue.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function foo() { this = value; }
>value : Symbol(value, Decl(assignmentLHSIsValue.ts, 1, 3))

this = value;
>this : Symbol(globalThis)
>value : Symbol(value, Decl(assignmentLHSIsValue.ts, 1, 3))

// identifiers: module, class, enum, function
Expand Down Expand Up @@ -116,6 +117,7 @@ foo() = value;

// parentheses, the containted expression is value
(this) = value;
>this : Symbol(globalThis)
>value : Symbol(value, Decl(assignmentLHSIsValue.ts, 1, 3))

(M) = value;
Expand Down
6 changes: 3 additions & 3 deletions tests/baselines/reference/assignmentLHSIsValue.types
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function foo() { this = value; }

this = value;
>this = value : any
>this : any
>this : typeof globalThis
>value : any

// identifiers: module, class, enum, function
Expand Down Expand Up @@ -159,8 +159,8 @@ foo() = value;
// parentheses, the containted expression is value
(this) = value;
>(this) = value : any
>(this) : any
>this : any
>(this) : typeof globalThis
>this : typeof globalThis
>value : any

(M) = value;
Expand Down
4 changes: 4 additions & 0 deletions tests/baselines/reference/castExpressionParentheses.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ declare var a;
(<any>null);
// names and dotted names
(<any>this);
>this : Symbol(globalThis)

(<any>this.x);
>this : Symbol(globalThis)

(<any>(<any>a).x);
>a : Symbol(a, Decl(castExpressionParentheses.ts, 0, 11))

Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/castExpressionParentheses.types
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@ declare var a;
(<any>this);
>(<any>this) : any
><any>this : any
>this : any
>this : typeof globalThis

(<any>this.x);
>(<any>this.x) : any
><any>this.x : any
>this.x : any
>this : any
>this : typeof globalThis
>x : any

(<any>(<any>a).x);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module a {
}
var f = () => this;
>f : Symbol(f, Decl(collisionThisExpressionAndAliasInGlobal.ts, 3, 3))
>this : Symbol(globalThis)

import _this = a; // Error
>_this : Symbol(_this, Decl(collisionThisExpressionAndAliasInGlobal.ts, 3, 19))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ module a {
>10 : 10
}
var f = () => this;
>f : () => any
>() => this : () => any
>this : any
>f : () => typeof globalThis
>() => this : () => typeof globalThis
>this : typeof globalThis

import _this = a; // Error
>_this : typeof a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ declare class _this { // no error - as no code generation
}
var f = () => this;
>f : Symbol(f, Decl(collisionThisExpressionAndAmbientClassInGlobal.ts, 2, 3))
>this : Symbol(globalThis)

var a = new _this(); // Error
>a : Symbol(a, Decl(collisionThisExpressionAndAmbientClassInGlobal.ts, 3, 3))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ declare class _this { // no error - as no code generation
>_this : _this
}
var f = () => this;
>f : () => any
>() => this : () => any
>this : any
>f : () => typeof globalThis
>() => this : () => typeof globalThis
>this : typeof globalThis

var a = new _this(); // Error
>a : _this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ declare var _this: number; // no error as no code gen

var f = () => this;
>f : Symbol(f, Decl(collisionThisExpressionAndAmbientVarInGlobal.ts, 1, 3))
>this : Symbol(globalThis)

_this = 10; // Error
>_this : Symbol(_this, Decl(collisionThisExpressionAndAmbientVarInGlobal.ts, 0, 11))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ declare var _this: number; // no error as no code gen
>_this : number

var f = () => this;
>f : () => any
>() => this : () => any
>this : any
>f : () => typeof globalThis
>() => this : () => typeof globalThis
>this : typeof globalThis

_this = 10; // Error
>_this = 10 : 10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ class _this {
}
var f = () => this;
>f : Symbol(f, Decl(collisionThisExpressionAndClassInGlobal.ts, 2, 3))
>this : Symbol(globalThis)

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class _this {
>_this : _this
}
var f = () => this;
>f : () => any
>() => this : () => any
>this : any
>f : () => typeof globalThis
>() => this : () => typeof globalThis
>this : typeof globalThis

Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ enum _this { // Error
}
var f = () => this;
>f : Symbol(f, Decl(collisionThisExpressionAndEnumInGlobal.ts, 4, 3))
>this : Symbol(globalThis)

Loading

0 comments on commit be2db9d

Please sign in to comment.