Interviews News

WebStorm 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.

balcony

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.

remove-override-modifier-warning

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:

  1. Add syntax support.
  2. Add semantics support: type inferences, reference resolutions, and so on.
  3. 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.

syntax-semantics-support

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.

override-support-completion

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.

completion-for-override

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.

false-positive-warning-webstorm

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.

styled-declaration

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.

inline-style-component

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.

component-props-type

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.

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.

type-bar

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 var keyword is used, WebStorm will suggest converting it to let or 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:

  1. esModuleInterop or allowSyntheticDefaultImports are turned on:
    1. If the imported module contains export =, import Foo from ‘bar’ should be used.
    2. 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.
  2. esModuleInterop or allowSyntheticDefaultImports are turned off:
    1. If the imported module doesn’t contain export =, import * as Foo from ‘bar’ should be used.
    2. 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.

convert-to-import-from-ts

Summary

So, today, I:

  1. Pushed 6 commits.
  2. Did 4 code reviews.
  3. Had 3 meetings, including our daily stand-up.
  4. Had 3 cups of coffee.
  5. 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

image description