microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.17k stars 12.51k forks source link

Is there a way to use TypeScript to prevent accidental global access? #14306

Open bcherny opened 7 years ago

bcherny commented 7 years ago

I just spent 2 hours tracking down this issue:

Is there a way to throw a compile time error when implicitly accessing global methods (on window, global, etc.)?

If not, a compiler flag would be extremely helpful. I would happily be more explicit about commonly used globals (window.setTimeout, window.document, ...) if it meant I could more easily catch insidious bugs like this one.

Full commit here: https://github.com/coatue/SlickGrid/commit/0f8bab3fa9968173d749ccdf535c88f3a526ca8b.

DanielRosenwasser commented 7 years ago

The number of times I use name and location as variables tends to make this pretty frustrating.

The unfortunate problem is that you can end up writing accidentally wrong code if we omit the global declarations (because you might accidentally redeclare global variables) so we'd really need to be able to recognize that there are undesirable properties declared that you don't want to access.

niieani commented 7 years ago

How about adding a tsconfig.json option such as:

explicitGlobals: true

With it on, if you'd write name, location or focus() without being explicit, i.e. window.name, while at the same time, if those names aren't declared in the local scope, it would mark those references as errors. At the same time there could be a quick fix to either reference the local scope, in case of a similar problem to @bcherny (missing this) or to reference the global scope explicitly, (i.e. name => window.name).

This would require being able to mark variables such as window, self and global as globals, instead of redefining each of their properties globally (as is currently done).

This would also mean that given the option is true, and one declared a $ global which is typeof jQuery, any usage would trigger the above error, unless you'd use it as window.$ or using import * as $ from 'jQuery'.

blakeembrey commented 7 years ago

That's a good idea and typical of JavaScript linting (e.g. the /* global $ */ header). If it were explicitGlobals you could set an array for the same effect (e.g. whitelisted globals). However, I wonder if this would be better as a TSLint rule instead. For instance, a rule like http://eslint.org/docs/user-guide/configuring#specifying-globals could be implemented. I saw https://github.com/palantir/tslint/issues/922, but no discussion of a "globals" option whitelist like JavaScript linters tend to have.

bcherny commented 7 years ago

The unfortunate problem is that you can end up writing accidentally wrong code if we omit the global declarations (because you might accidentally redeclare global variables)

@DanielRosenwasser AFAIK TS gives an error when declaring a variable without a var/let/const keyword, similar to how use strict enforces this at runtime. Eg. foo = 1 fails, but var foo = 1 is fine. How else could you accidentally overwrite a global? Would you mind helping me understand with a concrete example?

explicitGlobals: true... At the same time there could be a quick fix to either reference the local scope

@niieani That would be a very clean UX, I like these two ideas a lot.

This would require being able to mark variables such as window, self and global as globals, instead of redefining each of their properties globally (as is currently done).

Can you help me understand why? Shouldn't the fact that the declaration is ambient (as opposed to exported from a module) be enough to hint to TSC that a name is global?

However, I wonder if this would be better as a TSLint rule instead.

@blakeembrey That's an interesting idea - I wonder what makes a behavior belong in linterland vs. compilerland, semantically speaking. Perhaps since this sort of mistake causes incorrect behavior, it belongs in TSC rather than TSlint.

Jessidhia commented 7 years ago

Another name that is tricky, especially when converting old code, is self. Almost got bitten by that a few weeks ago...

zpdDG4gta8XKpMCd commented 7 years ago

workaround is to use "noLibs": true and then add necessary *.d.ts by hands from https://github.com/Microsoft/TypeScript/tree/master/src/lib

once done, you can delete unwanted declarations from say dom.d.ts by hands

downside though is that you will have to manually update theses files as typescript advances

zpdDG4gta8XKpMCd commented 7 years ago

@DanielRosenwasser it might worth considering adding an option to mute certain ambient declarations, something like:

undeclare let name: string;
lefb766 commented 7 years ago

1351 is related.

@bcherny Check out this issue for an example of the redeclaration problem (https://github.com/Microsoft/TypeScript/issues/1351#issuecomment-106573083).

Knagis commented 7 years ago

We ended up editing the lib.d.ts file and removing most of the globals to avoid using parent, name, top etc. by mistake.

Perhaps these globals could be separated in their own lib file that can then be selected by adding, for example, DOM.Globals to the lib setting in tsconfig.json? The default DOM would include only very few selected variables, such as window and document.

highruned commented 7 years ago

Keep running into this with name. Runtime errors :|

highruned commented 7 years ago

Another suggestion is anything inside a defined module doesn't inherit globals (that's the first thing I tried). I gather that may reduce the barrier to doing this so it won't be an all or nothing thing. Maybe combine with undeclare or omit or visa versa (opt into globals). eg

module MyModule {
  global name
  global focus

  export class Topic {
    constructor() {
      focus() // refers to global focus
    }
  }
}

I recall some language doing it like this.. maybe ActionScript or C.

evmar commented 7 years ago

We have also been bitten by the 'name' thing at Google, and have also been considering patching it out of our lib.d.ts. I think the fix that behaves how TypeScript does is to split the standard library up further so that a project can opt in or out from the global declarations.

ccorcos commented 6 years ago

I just had an issue where we refactored a function that called parent.close() and parent wasn't in scope anymore. It causes electron to freeze with no errors anywhere. What a pain!

It would be nice if we could whitelist globals inside TSConfig. I don't want accessing anything on the window object without explicitly calling window.property.

Another option would be to require importing any globals. Not sure where it would import it from, but something like import window from "ts/lib/dom" would solve this issue as well.

ianks commented 6 years ago

i just got hosed by name. it would awesome if there was away to limit access to these to the window object.

zpdDG4gta8XKpMCd commented 6 years ago

as of now the only way is to take ownership of lib.d.ts by fixing it, ask me how

a7madgamal commented 6 years ago

how about a new setting or lib to allow these globals ONLY through window.xxx

poke commented 5 years ago

What about creating a separate lib.dom-strict.d.ts that only declares the global variables window and document? So any use to e.g. name would have to go through window.name?

So users could choose to reference that strict version of the dom library to avoid these global names from being used within their applications.

mac2000 commented 5 years ago

Looking for a way to prevent usage (not declaration) of global variables to have proper IoC and DI in place in Angular.

E.g. wish to prevent usage of localStorage and force its injection via constructor

ccorcos commented 5 years ago

For now, I just have a post-install script that rewrites type definition files for malicious files.

rewrite("node_modules/@types/jest/index.d.ts", str => {
    return str
        .replace(/declare const/g, "export const")
        .replace(/declare var/g, "export var")
        .replace(/declare function/g, "export function")
})
csicky commented 5 years ago

image

The autocomplete list has nothing to do with my work, I don't know why is there, I don't know what the focus function is and why is on top. All the items are like random words and I have no idea where are coming from. I have disabled auto import but still what I see in this list is disturbing. I try to imagine why are all these items there but can't find a reason. It's just random garbage.

I don't know why it's so complicated, really. What is on window and what is in my code. Nothing more, nothing less. My variables to be on top, those from window to have less priority.

RyanCavanaugh commented 5 years ago

I don't know why is there, I don't know what the focus function is

I don't know why it's so complicated, really. What is on window and what is in my code. Nothing more nothing less

focus comes from window. TypeScript is literally doing what you said it should do, minus the sorting part.

csicky commented 5 years ago

Yes, you are right, the screenshot has most of the things from window. I hate this autocomplete because of what it did in the past, I've had so many exotic things in it. It seems that disabling the auto import feature works well indeed.

I don't know how it does it, but Visual Studio was giving me exactly what I needed at the right time. I use Visual Studio Code since many years now, but I never have a good experience with what it suggests. If Visual Studio would work well with Vue.js I wouldn't be here writing about fixing Code.

Sorry for my tone, this is just real feedback, without the feelings it would be just half the feedback.

cazgp commented 5 years ago

Was just also bitten by the name problem.

KurtPreston commented 5 years ago

There is a new TSLint rule that prevents access to certain global vars:

https://palantir.github.io/tslint/rules/no-restricted-globals/

cazgp commented 5 years ago

Still seems like it's something the compiler should be able to handle.

trotyl commented 5 years ago

undefined is also a normal global variable in JavaScript (some people may not even aware of that), and every access to [[global]].undefined is not accidental, but deliberate.

From the thread most people only want to forbid HTML-extension of global, rather than JavaScript global itself.

csicky commented 5 years ago

It's not that important what is there, it's what is selected. In Visual Studio is almost like it's reading my mind. With VS Code is like it is only now learning what it should be.

For example, in VS if I write something.whatever = other.whatever as soon as I type o , other is highlighted and with that o typed I then type . the other is completed, and, here is the magic, whatever is already selected on other. This is just one example of many IntelliSense is capable off. Being in the same Microsoft, why VS Code team don't ask for a little help from the VS team?

We had good IntelliSense in VS in 2005 already.

Sorry for my English :)

NN--- commented 5 years ago

@KurtPreston Since TSLint is deprecated, there is a same rule in ESLint and even eslint-restricted-globals npm package with list of confusing globals such as name.

AlexGalays commented 5 years ago

This is SO valuable and has been a source of bugs, confusion and generally lost time on most projects.

Examples of globals with totally stupid names that are easily used instead of a local version: find (only a problem in JS project however), name, close, history, length, open, parent, scroll, self, stop, event, ALL event listener attributes, etc.

inca commented 5 years ago

+1 for dom-strict, +1 for globals: ['window', 'document'] tsconfig option — whichever one is more feasible. But we definitely need something.

jahooma commented 5 years ago

We had two bugs this week because of this. Why doesn't typescript have a solution out of the box? Seems like an obvious addition for strict mode.

jahooma commented 5 years ago

Ok, while typescript is working on this, here's a solution.

If you use eslint, you can just copy the below rule into your .eslintrc.

I left out some of the more commonly used variables like document, setTimeout, and addEventListener, but otherwise it is most of them.

"no-restricted-globals": ["error","postMessage","blur","focus","close","frames","self","parent","opener","top","length","closed","location","origin","name","locationbar","menubar","personalbar","scrollbars","statusbar","toolbar","status","frameElement","navigator","customElements","external","screen","innerWidth","innerHeight","scrollX","pageXOffset","scrollY","pageYOffset","screenX","screenY","outerWidth","outerHeight","devicePixelRatio","clientInformation","screenLeft","screenTop","defaultStatus","defaultstatus","styleMedia","onanimationend","onanimationiteration","onanimationstart","onsearch","ontransitionend","onwebkitanimationend","onwebkitanimationiteration","onwebkitanimationstart","onwebkittransitionend","isSecureContext","onabort","onblur","oncancel","oncanplay","oncanplaythrough","onchange","onclick","onclose","oncontextmenu","oncuechange","ondblclick","ondrag","ondragend","ondragenter","ondragleave","ondragover","ondragstart","ondrop","ondurationchange","onemptied","onended","onerror","onfocus","oninput","oninvalid","onkeydown","onkeypress","onkeyup","onload","onloadeddata","onloadedmetadata","onloadstart","onmousedown","onmouseenter","onmouseleave","onmousemove","onmouseout","onmouseover","onmouseup","onmousewheel","onpause","onplay","onplaying","onprogress","onratechange","onreset","onresize","onscroll","onseeked","onseeking","onselect","onstalled","onsubmit","onsuspend","ontimeupdate","ontoggle","onvolumechange","onwaiting","onwheel","onauxclick","ongotpointercapture","onlostpointercapture","onpointerdown","onpointermove","onpointerup","onpointercancel","onpointerover","onpointerout","onpointerenter","onpointerleave","onafterprint","onbeforeprint","onbeforeunload","onhashchange","onlanguagechange","onmessage","onmessageerror","onoffline","ononline","onpagehide","onpageshow","onpopstate","onrejectionhandled","onstorage","onunhandledrejection","onunload","performance","stop","open","print","captureEvents","releaseEvents","getComputedStyle","matchMedia","moveTo","moveBy","resizeTo","resizeBy","getSelection","find","createImageBitmap","scroll","scrollTo","scrollBy","onappinstalled","onbeforeinstallprompt","crypto","ondevicemotion","ondeviceorientation","ondeviceorientationabsolute","indexedDB","webkitStorageInfo","chrome","visualViewport","speechSynthesis","webkitRequestFileSystem","webkitResolveLocalFileSystemURL", "openDatabase"],

timo-kang commented 4 years ago

:(

jgehrcke commented 4 years ago

+1 for dom-strict, +1 for globals: ['window', 'document'] tsconfig option — whichever one is more feasible. But we definitely need something.

Yes, we need something. In tsc. We got bitten by this kind of problem more than once. It's quite disappointing to see TSC not catching this, currently.

Ok, while typescript is working on this, here's a solution.

Thanks for that @jahooma.

cmawhorter commented 3 years ago

thanks to @jahooma. added some formatting:

    "no-restricted-globals": [
      "error", "postMessage", "blur", "focus", "close", "frames", "self",
      "parent", "opener", "top", "length", "closed", "location", "origin",
      "name", "locationbar", "menubar", "personalbar", "scrollbars",
      "statusbar", "toolbar", "status", "frameElement", "navigator",
      "customElements", "external", "screen", "innerWidth", "innerHeight",
      "scrollX", "pageXOffset", "scrollY", "pageYOffset", "screenX", "screenY",
      "outerWidth", "outerHeight", "devicePixelRatio", "clientInformation",
      "screenLeft", "screenTop", "defaultStatus", "defaultstatus",
      "styleMedia", "onanimationend", "onanimationiteration",
      "onanimationstart", "onsearch", "ontransitionend",
      "onwebkitanimationend", "onwebkitanimationiteration",
      "onwebkitanimationstart", "onwebkittransitionend", "isSecureContext",
      "onabort", "onblur", "oncancel", "oncanplay", "oncanplaythrough",
      "onchange", "onclick", "onclose", "oncontextmenu", "oncuechange",
      "ondblclick", "ondrag", "ondragend", "ondragenter", "ondragleave",
      "ondragover", "ondragstart", "ondrop", "ondurationchange", "onemptied",
      "onended", "onerror", "onfocus", "oninput", "oninvalid", "onkeydown",
      "onkeypress", "onkeyup", "onload", "onloadeddata", "onloadedmetadata",
      "onloadstart", "onmousedown", "onmouseenter", "onmouseleave",
      "onmousemove", "onmouseout", "onmouseover", "onmouseup",
      "onmousewheel", "onpause", "onplay", "onplaying", "onprogress",
      "onratechange", "onreset", "onresize", "onscroll", "onseeked",
      "onseeking", "onselect", "onstalled", "onsubmit", "onsuspend",
      "ontimeupdate", "ontoggle", "onvolumechange", "onwaiting", "onwheel",
      "onauxclick", "ongotpointercapture", "onlostpointercapture",
      "onpointerdown", "onpointermove", "onpointerup", "onpointercancel",
      "onpointerover", "onpointerout", "onpointerenter", "onpointerleave",
      "onafterprint", "onbeforeprint", "onbeforeunload", "onhashchange",
      "onlanguagechange", "onmessage", "onmessageerror", "onoffline",
      "ononline", "onpagehide", "onpageshow", "onpopstate",
      "onrejectionhandled", "onstorage", "onunhandledrejection", "onunload",
      "performance", "stop", "open", "print", "captureEvents",
      "releaseEvents", "getComputedStyle", "matchMedia", "moveTo", "moveBy",
      "resizeTo", "resizeBy", "getSelection", "find", "createImageBitmap",
      "scroll", "scrollTo", "scrollBy", "onappinstalled",
      "onbeforeinstallprompt", "crypto", "ondevicemotion",
      "ondeviceorientation", "ondeviceorientationabsolute", "indexedDB",
      "webkitStorageInfo", "chrome", "visualViewport", "speechSynthesis",
      "webkitRequestFileSystem", "webkitResolveLocalFileSystemURL",
      "openDatabase",
      // adding common in. any others?
      "window", "document", "setTimeout", "setInterval", "addEventListener",
      "removeEventListener",
    ],
ghost commented 3 years ago

The unfortunate problem is that you can end up writing accidentally wrong code if we omit the global declarations (because you might accidentally redeclare global variables) so we'd really need to be able to recognize that there are undesirable properties declared that you don't want to access.

No ES runtime starts with "global variables," these are all properties of the global object, therefore this is just a dom lib problem, +1 for dom-strict.

alamothe commented 3 years ago

How do we patch lib.dom.d.ts? Where is it located?

david-fong commented 3 years ago

How do we patch lib.dom.d.ts? Where is it located?

If you've installed typescript as a dependency of a project, it will be in node_modules/typescript/lib/lib.dom.d.ts. Some IDE's will ship with a recent version of TypeScript, which probably isn't a good idea to touch since your changes will be lost every time the IDE updates the version of TypeScript it ships with.

david-fong commented 3 years ago

Picking up from a discussion in 18433, quoting @thw0rted:

I'm not an expert but I think if you fork lib.dom, or split it such that some people are using "bad parts" of the global namespace while others aren't, you open yourself up to the same issues. Let's say I depend on some-lib and it includes a call to doStuff(name), intentionally referring to window.name using shorthand. They built their lib using an older version of TS, or they consciously included lib.dom-strict.d, but your consuming project avoids including the "strict" lib. Without skipLibCheck, the typechecker is going to descend into your dependency and flag the reference to name as an error.

I'm not an expert either, so I'd appreciate someone jumping in to confirm: I think the lib check is just for .d.ts files and not on source code files, and a project would need to set checkJs for that to happen. So I don't think the scenario you mention above will happen, since type definition files typically need a triple-slash lib reference to get dom types (where the author can choose lib.dom or lib.dom-strict), unless the author includes the tsconfig in their published package, which I think is probably a mistake.

This can be seen in DefinitelyTyped, where packages that triple-slash for the dom lib are: auth0-js, bent, fetch-headers, interface-datastore, make-fetch-happen, node-red__editor-client, pdfjs-dist, pdfmake, react-map-gl, requestidlecallback, socket.io, superagent, and xmldom.

To avoid duplicate definitions, I think lib.dom.d.ts can be set to triple-slash type reference lib.dom-strict.d.ts, and then just add the globals.

crfrolik commented 3 years ago

This is a pretty big footgun. We had a bug released to the wild due to this. I know that I'm suffering from recency bias, but I think this should be near the top of the TypeScript team's priority list.

Aaron-Pool commented 3 years ago

Adding another real world example to this. My team accidentally had a window.stop call released into production. It was in a cleanup function, when a particular DOM node was destroyed. The function that was intended to be called had been renamed from stop to stopPolling. It was insanely hard to track down why requests were randomly getting cancelled. I would love for there to be some rule to force explicit usage of window.* functions, rather than accidentally calling them as global methods.

muhamedkarajic commented 2 years ago

I don't know why is there, I don't know what the focus function is

I don't know why it's so complicated, really. What is on window and what is in my code. Nothing more nothing less

focus comes from window. TypeScript is literally doing what you said it should do, minus the sorting part.

Seriously the sorting is the most important thing. When I program I would like to not have to go trough the hole list of suggestions and pick up where my variable is - ironically it's making me even slower. Just finally sort it.

thw0rted commented 2 years ago

The sorting part (of Intellisense) is configurable in your user / workspace preferences. I do think that the best solution for this ticket is to use the linter rule discussed above, but you might want to file a ticket with VSCode or the TS language service to allow users to have an ignore list that removes specific symbols from autocomplete.

envatic commented 2 years ago

extend the "eslint:recommended" in your eslintrc add it to extend extends: ["eslint:recommended", "plugin:vue/vue3-recommended", "prettier"],

fo-fo commented 2 years ago

Never was a fan of globals, and issues like this don't make me any more of a fan. Would be nice to see some kind of a solution for this. My preferred solution would be to have to explicitly import all globals, although I guess such explicit imports which would be omitted from the generated JS code might not align with TypeScript's goals.

An additional problem is that auto-import in VSCode doesn't offer any import suggestions if the symbol's name happens to match some global. One such example is the Text component in React Native (there's also a Text interface/constructor in DOM.)

zardoy commented 1 year ago

https://github.com/microsoft/TypeScript/issues/14306#issuecomment-484579261

Speaking of autocomplete, I'm using this (maybe someone else also would find it handy):

super

On the right side it also can be lib.es5 or lib.es2020 depending from which lib it comes

https://github.com/microsoft/TypeScript/issues/51936

alamothe commented 1 year ago

TypeScript now supports custom DOM library: https://devblogs.microsoft.com/typescript/announcing-typescript-4-5/#lib-node-modules

So someone only needs to publish the library without globals.

The other way is to take lib.dom.d.ts, edit it to remove globals and save it with your project. Then remove a reference to the DOM library from your tsconfig.json. This approach also works great.

Aaron-Pool commented 1 year ago

Adding another real world example to this. My team accidentally had a window.stop call released into production. It was in a cleanup function, when a particular DOM node was destroyed. The function that was intended to be called had been renamed from stop to stopPolling. It was insanely hard to track down why requests were randomly getting cancelled. I would love for there to be some rule to force explicit usage of window.* functions, rather than accidentally calling them as global methods.

Two years later, and my team just got bit by the same issue, but with window.close() this time, would still love to see a stricter lib.dom.d.ts version published that remove window.* functions form the global namespace.

smirea commented 1 year ago

+1 here, the number of times I accidentally autocompleted onchange() from global lib/dom instead of the local onChange handler are too many to recall

If you use pnpm as the package manager, you can use the pnpm patch command to edit typescript/lib/lib.dom.d.ts and comment most of the declare var and declare function declarations towards the end of the file.

danvk commented 10 months ago

Another option would be to mark globals like focus as @deprecated like Daniel did for name in https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/883. That way they'd at least be visibly struck though in your editor, a pretty good indication that something weird is going on!