Under the Hood: A Day in the Life of a WebStorm Developer
We recently published an interview with Ekaterina Prigara, our product manager, in which we shed some light on how we build WebStorm. Since it seemed to strike a chord with you, we talked another member of our team into doing part 2. Have you ever wanted to learn what it takes to be an IDE developer? Read on to find out!
Intro
Hi everyone! I am Andrey Starovoyt, the WebStorm team lead. As is often the case at JetBrains, I also work as a developer, programming with Java and Kotlin since WebStorm is a JVM-based application.
People often ask me about the language support in WebStorm: they want to know how it works, what it looks like on the inside, and so on. I’m going to answer these and some other related questions by walking you through a day in my life as a developer on the WebStorm team. Let’s get started!
Task 0: Catching up
I start my day by going to the office (which is my balcony at the moment – we’re still working remotely), getting a cup of coffee, and catching up on the new emails in my inbox: issues, comments from users, status updates, and the like.
Of course, emails and other distractions will try to hunt me down throughout the day, as well: there’s nowhere to hide! There are always going to be meetings to attend, emails to read, and Slack messages to respond to when you work in a team. I won’t write about this part of my job, though, and instead, I’ll focus on my developer tasks for the day. Let’s start with task 1.
Task 1: TypeScript 4.3, “override” modifier support
I start off with my most important task of the day. Version 4.3 of TypeScript has the new
override modifier, and we want to support it in WebStorm along with the
noImplicitOverride flag because we know that our users like working with the most recent versions of technologies.
First, I should give you some background about how support for the various languages works in our IDEs. I’ll use TypeScript as an example.
You may know that our IDEs are extremely feature-rich. There are refactorings, quick-fixes, inline hints, and support for frameworks like React, Angular, and Vue, to name just a few of the features. We have our own model of the language, with its own lexer, parser, and type inference rules. This means that we have to update the internal model every time a new version of the language comes out.
For TypeScript, in addition to the language support built using the WebStorm mechanisms, we also have an integration with the TypeScript Language Service. We use it to show errors from the compiler in the editor and to add quick-fixes from the TypeScript Language Service on top of what is already built into WebStorm.
Let’s get back to the task. To support a new feature like this, we need to do three things:
- Add syntax support.
- Add semantics support: type inferences, reference resolutions, and so on.
- Update the existing features that may be affected by this change.
Syntax and semantics support
I start with an easy task – implementing syntax and semantics support. For the syntax part, I need to add the
override keyword to the list of supported modifiers and make sure the parsing order is consistent with the implementation on the TypeScript compiler side. I also need to enable context keyword highlighting for the syntax. It can’t be done at the lexing stage because some contexts allow
override as an identifier, and we don’t want to highlight it as a keyword in such cases.
Moving on to semantics. The new modifier only reports errors without changing any behavior, so I don’t need to make changes to our model to support it. We usually add quick-fixes on this step, but since the TypeScript Language Service has its own add / remove ‘override’ modifier quick-fixes, there’s no need to implement them on the WebStorm side.
Updating the existing features
The last thing that I have to do is integrate this change with the existing features, which use information from our own language model. Specifically, I need to make sure all the actions related to code generation understand the
noImplicitOverride flag and add an
override.
Let’s start with the actions related to code generation. There are two of them: the Override Method refactoring and the code completion that works in a similar way by generating a method. I modify the actions slightly: they will now read the corresponding tsconfig.json file and add the modifier when it’s necessary.
Now, let’s add the
override keyword to the top-level code completion of classes, so it will work like
private and
protected do. Another thing I want to do is to show completion suggestions after the
override modifier, filtering out the methods that can be overridden.
That’s it for this task! The next steps are to add some tests for refactoring and code completion changes and to commit and push my changes. After the tests are marked as passed on the build server, the bot automatically merges the changes to the main branch and I close the corresponding issue in YouTrack.
Task 2: Fixing bugs
Moving on to the next task. There’s a warning about a missing attribute for a Material-UI component wrapped with styled components. The code is correct, and the TypeScript compiler doesn’t report any errors there. This makes me think that the problem is on the WebStorm side and is related to the differences between TypeScript and our own language models.
This bug is a regression – I can only reproduce the bug in the new version of WebStorm. The previous version works fine. Luckily, the problem was reported by one of our users during the EAP, so I can still fix it before the stable release.
I start by looking at the types to understand where the behavior of the IDE is different from the TypeScript compiler. Going through the typings for styled components together with Material-UI isn’t easy. For example, here is what a declaration of the
styled`` function looks like.
As it’s nearly impossible to read or debug such typings, I try to narrow the problem down to a smaller block of code. First, let’s inline the
<StyledButton
> type, to simplify the code.
With this change, I can no longer run the code in the sample. I decide not to worry about this for now because the first thing I need to do is to check the type inference. I’ll make sure everything works as expected later.
When you add
<StyledButton>, TypeScript expands the corresponding type and searches for
call or
new signatures. The first parameter of the signatures is used for the props type.
After expanding numerous aliases and applying generic arguments, the
Button component for Material-UI has three signatures. One of them specifies the
href property as optional. This is why everything works correctly for the original
Button component from Material-UI.
After wrapping, the component has two signatures, and neither of them should be used with the specified props.
Now I need to find a place within the styled component typings where the original call signatures are transformed into the new ones. I take a closer look at styled-components typings and find out that the
ComponentProps type is responsible for the transformation.
ComponentProps takes a type as the generic parameter. If the type has a
call or
new signature, it extracts the type of the signature’s first parameter. Maybe
ComponentProps works differently on the WebStorm side compared to the TypeScript compiler? Let’s check the abstract type
Bar.
For the
Bar type, WebStorm infers
{ href?: string }, so it’s just a union type of the props parameters for the first and second signature. The TypeScript compiler understands that we need to keep the first parameter optional, as it is in a union type, but we also need to add a
{somethingElse: string} to the result type, and in this part it works as an intersection type!
I decide to check how the TypeScript compiler handles the processing of several call signatures inside a type transformation. I open the source code of the TypeScript compiler, take the same code sample, and start the debugger. To my surprise, I discover that the TypeScript compiler uses only the latest signature in this case!
I didn’t see this coming. I want to make sure that it is supposed to work this way, so I try to flip the signature in the
Bar type and check the result type.
The TypeScript compiler’s result is
{ href: string }. Now I understand the culprit: WebStorm uses the union type of the call signatures, while the TypeScript compiler uses only the last call signature. I fix the inconsistency, ensure the code can be run again, add the tests, and then commit and push my changes.
Task 3: A new intention for TypeScript
WebStorm has three features that can automatically change code:
- Quick-fixes. These actions are associated with inspections, which find and report problems with your code. The problems can range from critical errors to code style issues. The quick-fixes let you change the code to solve them. For instance, when the
varkeyword is used, WebStorm will suggest converting it to
letor
const.
- Intentions. These actions allow you to transform your existing code in some way or another. For example, you can convert a regular function to an arrow function.
- Refactorings. With refactorings, you can reorganize your code without changing the way it works. They are similar to intentions but work on a larger scale and often affect several files. Here’s an example of a popular refactoring.
As part of my last task for today, I’m going to work on a new intention that allows you to convert
import =
require to
import from. We already have a similar intention for JavaScript, so I decided to implement it for TypeScript, too. At first, it looks like it’s going to be a no-brainer. We have this syntax:
import Foo = require(‘bar’)
And we want to convert it to the following:
Import Foo from ‘bar’
In reality, the implementation part is much harder than it looks. The syntax with the default import is valid only if the
bar module has a default export. If it doesn’t and the options
esModuleInterop or
allowSyntheticDefaultImports aren’t enabled in tsconfig.json, you’ll get a compilation error. It’s also important to check whether the symbol is imported from a d.ts file. So, here are the cases the new intention should cover:
esModuleInteropor
allowSyntheticDefaultImportsare turned on:
- If the imported module contains
export =,
import Foo from ‘bar’should be used.
- If the imported module doesn’t contain
export =, then:
- If the current file is a declaration, or .d.ts,
import Foo from ‘bar’should be used.
- If it isn’t,
import * as Foo from ‘bar’should be used.
esModuleInteropor
allowSyntheticDefaultImportsare turned off:
- If the imported module doesn’t contain
export =,
import * as Foo from ‘bar’should be used.
- If the imported module contains
export =, then:
- If the module is used to the right of
export =,
import * as Foo from ‘bar’should be used.
- If the module isn’t used to the right of
export =, convertion shouldn’t work at all.
Now, I need to make sure the new intention actually covers all these cases. Next, I need to call Optimize imports to ensure the import is merged with other ES imports if they exist. Then I add tests to check the new feature, and I commit and push my final changes for today.
Summary
So, today, I:
- Pushed 6 commits.
- Did 4 code reviews.
- Had 3 meetings, including our daily stand-up.
- Had 3 cups of coffee.
- Wrote:
- Task 1: [+154, -37] lines of code for the fix, and [+404, -26] lines of code for 10 tests.
- Task 2: [+32, -30] lines of code for the fix, and [+123, -2] lines of code for 4 tests.
- Task 3: [+145, -0] lines of code for the fix, and [+116, -1] lines of code for 6 tests.
I hope this blog post gave you some interesting insight into how things work inside our team. Of course, the tasks we work on aren’t limited to TypeScript support – we get to work on dozens of subsystems and numerous types of tasks. If there is anything you’re particularly interested in hearing about, let us know in the comments below.
The WebStorm team