Skip to content

Golar Gets a Linter. What's Next?

Golar first started as a direct wrapper around tsgo CLI. However, I wanted to add a linter to it from the first day.

And I’m happy to announce that Golar finally gets its own experimental linter!

In this blog post I’m going to explain how Golar integrates linting, what Golar’s current positioning is, and how I see its future.

I separate web-development language tooling into four categories.

  • Syntax-only linting — Traditional AST only linting. It can catch a lot of obvious mistakes in code, however, it’s rather limited due to the lack of type-awareness.
  • Type-aware lintingLinting with TypeScript type information is a superset of syntax-only linting. It can catch more mistakes and do it more accurately.
  • Type-checking — catches type errors in TypeScript code.
  • LSP features — autocompletion, hover, go-to-definition, showing errors in a text editor and so on.

Comprehensive support for each of these categories is essential for high-quality web-development language tooling.

We’ll also consider two groups of languages:

  • Supported by TypeScript natively (JavaScript/TypeScript, JSX/TSX) — these languages don’t require any extensions to the official TypeScript compiler.
  • Custom TS-based languages (Astro, Svelte, Vue, and others) — the TypeScript compiler is unaware of these languages and must be extended in order to support them.

For a very long time, all web-dev tooling — including the TypeScript compiler — was written in JavaScript. And while it provided great developer experience, it was rather slow.

JS-based tooling had evolved to support almost every part of the TypeScript-based language tooling landscape:

FeatureJavaScript/TypeScriptAstro, Svelte, Vue, and others
Syntax-only linting
Type-aware linting⚠️
Type-checking
LSP features

✅ — Supported

⚠️ — Limited support

Syntax-only linting was solved by ESLint, typescript-eslint and ESLint plugins (eslint-plugin-astro, eslint-plugin-svelte, eslint-plugin-vue).

Type-aware linting of JS/TS was solved by typescript-eslint. However, fully type-aware linting of TS-based languages was supported only partially. In particular, it is unable to resolve imports of .astro, .svelte, or .vue files when @typescript-eslint/parser is used.

Another problem is that only script parts of these languages were checked. This means that JS expressions contained in Astro, Svelte, and Vue templates can’t be linted with TypeScript type information.

Type-checking and LSP features were solved by Volar.js (used by Astro Language Tools and Vue Language Tools) and custom language-specific TypeScript tooling (Svelte Language Tools).

However, the ecosystem then started leaning towards native-speed tooling. Rust-based linters had emerged, and finally, typescript-go came out!

By “new-wave” tooling, I mean Biome, Oxlint + oxlint-tsgolint, and typescript-go.

FeatureJavaScript/TypeScriptAstro, Svelte, Vue, and others
Syntax-only linting
Type-aware linting⚠️ or ❌
Type-checking
LSP features

✅ — Supported

⚠️ — Limited support

❌ — Unsupported

Syntax-only linting for all JavaScript and TypeScript works great (and super fast!) in Rust-based linters. Linting of Astro, Svelte, and Vue is also already almost solved, mainly because there are no real technical barriers there.

Type-aware linting is a bit more difficult. Since typescript-go is written in Go, it’s non-trivial to integrate it with existing Rust-based linters. However, oxlint-tsgolint bundles typescript-go and supports type-aware linting of JavaScript and TypeScript via rules written in Go. Biome uses custom Rust-based type inference, which works great in most cases (although it’s still not the official TypeScript).

Type-aware linting of Astro, Svelte, and Vue is not yet fully solved. Biome is able to lint script parts of these languages with its custom type inference technology, however, template parts are not covered. Flint supports fully type-aware linting of Astro, Svelte, and Vue, but it currently uses TypeScript 5.9.

Type-checking for JavaScript and TypeScript is solved by typescript-go itself. The community built several tools that add type-checking for Svelte (svelte-check --tsgo, svelte-fast-check, svelte-check-rs) and Vue (vue-tsgo, vize, verter). These tools focus on supporting type-checking of a single TS-based language.

LSP features for JavaScript/TypeScript work without any flaws thanks to the built-in typescript-go LSP server. LSP support of Astro, Svelte, and Vue is not yet solved because typescript-go doesn’t provide an extensibility mechanism. See microsoft/typescript-go#648 and microsoft/typescript-go#2824.

Instead of focusing on providing linting or type-checking support for a particular TS-based language, Golar goes further and builds a language-agnostic pluggable architecture that supports the entire range of tooling areas. This means that Golar itself doesn’t support languages other than JavaScript and TypeScript. Instead, Golar plugins extend the base Golar functionality with custom TS-based language support.

Today, that full vision is still a technical preview rather than a finished platform. Type-checking is already the most stable part of Golar, while linting APIs, custom rules, and future LSP integrations are still evolving and may change between releases.

FeatureJavaScript/TypeScriptAstro, Svelte, Vue, and others
Syntax-only linting
Type-aware linting
Type-checking
LSP features

Golar is going to support pluggable:

  • TypeScript codegen — it’s required for TS-based languages type-checking and type-aware linting
  • Linter rules — plugins can add language-specific linting support
  • LSP extensibility — plugins can extend editor support for custom languages, including diagnostics, completions, hovers, and go-to-definition

Due to the nature of typescript-go, Golar provides several communication methods for plugins.

  • JS — plugin is authored in pure JS. It doesn’t require an additional compilation. The performance is slightly worse than native-speed code.
  • Rust — plugin can be authored in Rust. It requires compilation. The performance overhead is negligible.
  • IPC — plugin can be authored in any language. Golar spawns the plugin as a separate process and communicates with it via STDIO.
FeatureJSRustIPC
TypeScript codegen🚧
Syntax-only linter rules🚧
Type-aware linter rules
LSP extensibility🚧🚧

✅ — Supported

🚧 — Work in progress

❌ — Won’t support

Now, let’s take a look at how Golar components work.

Golar core (golar) bundles typescript-go as well as widely used JavaScript/TypeScript lint rules. This helps to achieve the best possible performance in both type-checking and linting.

Golar core exposes several communication channels that can be used to:

  • Provide type-checking support for custom TS-based languages (Astro, Svelte, Vue)
  • Provide custom lint rules
  • Request type information from the TypeScript compiler (can be used by both the LSP server and lint rules)

The main benefit of this approach is that type-checking, linting, and LSP features can reuse type information that was already computed by the TypeScript checker. That makes the whole system faster and helps keep memory usage lower.

Golar plugins, in this case @golar/vue, provide type-checking support, LSP support, built-in lint rules, and helpers for authoring custom third-party type-aware Vue rules.

In this particular example, the @golar/vue plugin provides all the necessary pieces to enable full support for Vue. However, this is not a strict rule. In fact, type-checking, LSP support, and linter extensions can be provided by completely different plugins!

Golar architecture

Golar architecture

One of Golar’s unique features is support for authoring custom type-aware (powered by typescript-go) lint rules in JS and Rust. Since this feature is not supported by any other linter at the moment, I want to say a little more about it.

The core idea of these plugins is based on my recent research — “Hybrid Type-Aware Linting: Performance Evaluation”. It showed that the “FFI Sync” approach is the most performant way to communicate with typescript-go.

Below is an example custom JS rule that uses type information from typescript-go. It reports all fn() calls where fn has any type.

import { defineRule } from 'golar/unstable'
import {
isCallExpression,
TypeFlags,
type CallExpression,
} from 'golar/unstable-tsgo'
export const jsRule = defineRule({
name: 'js/unsafe-calls',
setup(ctx) {
function checkCall(node: CallExpression) {
const type = ctx.program.getTypeAtLocation(node.expression)
if (type == null) {
return
}
if ((type.flags & TypeFlags.Any) !== 0 && type.intrinsicName === 'any') {
ctx.report({
message: 'Unsafe any call.',
range: {
begin: node.expression.pos,
end: node.expression.end,
},
})
}
}
ctx.sourceFile.forEachChild(function visit(node) {
if (isCallExpression(node)) {
checkCall(node)
}
node.forEachChild(visit)
})
},
})

Since Golar core is compiled to a shared library (.node addon), JS can communicate with it via Node-API. To minimize the communication overhead, JS and Go use shared raw memory to exchange large data, such as the AST. To further improve performance, JS rules are multithreaded by default. This means that several Node.js worker threads are running simultaneously, allowing them to take advantage of typescript-go’s built-in parallel checking capabilities.

And here is the equivalent rule written in Rust:

use golar::*;
fn check_call<'a>(ctx: &RuleContext<'a>, node: CallExpression<'a>) {
let Some(typ) = node
.expression()
.and_then(|expression| ctx.program.get_type_at_location(&expression))
else {
return;
};
if let Some(t) = typ.cast::<IntrinsicType>() && t.intrinsic_name() == "any" {
ctx.report_node(node.as_node(), "Unsafe any call.");
}
}
fn run<'a>(ctx: &RuleContext<'a>) {
walk(ctx.source_file.as_node(), &mut |node: Node<'_>| {
if let Some(call) = node.cast::<CallExpression>() {
check_call(ctx, call);
}
false
});
}
inventory::submit! {
Rule {
name: "rust/unsafe-calls",
run,
}
}

Rust plugins don’t need to parse or reconstruct the AST on their side, because they can read it directly from Go-managed memory. Thus, the only communication overhead between Rust plugins and Golar is CGO overhead.

At the moment, Rust lint plugins aren’t multithreaded. However, this is relatively easy to add, so in the near future they’ll be parallel as well, just like JS plugins.

Type-checking and declaration emit are already quite stable (see the getting started guide if you want to use them to speed up type-checking in your project), but linting and LSP support are still under active development. You can already try writing custom JS and Rust rules, but breaking changes are still possible. I’ll document them in Golar releases on GitHub, so if you update Golar, make sure to check the release notes.

There is still a lot to do before Golar is production-ready. These are the main areas I want to work on next.

My first priority is making the Golar LSP usable for daily work in Astro, Svelte, and Vue projects. The first step is to support the main TypeScript editor features: diagnostics, autocompletion, hovers, go-to-definition, and similar basics.

This first version will focus only on TypeScript-related features. It will not include language-specific features like HTML or CSS completions inside component templates yet.

After that, I want to improve the editor experience inside template and style sections too. That means adding completions, hovers, and other useful features for embedded HTML and CSS.

The goal is to make single-file components feel well supported, not just their JavaScript/TypeScript parts.

Custom lint rules are already an important part of Golar, but there is still a lot to improve. I want to expose more of the TypeScript Checker API to JS and Rust rules, make rule authoring easier, and improve performance.

The goal is to make custom rules both more powerful and easier to write.

I also plan to keep adding more built-in rules to Golar. Similar to what Flint is aiming for, I want Golar core to eventually include a large set of rules out of the box.

That includes not only JavaScript, TypeScript, Astro, Svelte, and Vue, but also formats such as Markdown, JSON, YAML, HTML, and CSS.

Another area I want to work on is project-aware linting: rules that can understand multiple files instead of looking at each file separately. This would make more advanced analysis possible, including cross-file diagnostics and fixes.

This would help people write more powerful rules for larger projects.

I also want to investigate what Angular and MDX support could look like in Golar. At the same time, I want to document the language plugin system better, so other people can add support for their own languages through Golar plugins.

If you want to try Golar today, start with the getting started guide and then open the language-specific setup docs for Astro, Ember, Svelte, or Vue.

In the meantime, if you want your Astro, Ember, Svelte, or Vue type-checking to benefit from typescript-go speed, you can try using Golar for it! (I believe this part of Golar is already pretty stable since it relies on official language toolings).

If you want to help me allocate more time on working Golar and continuing evolving web-dev language tooling, you can sponsor me on GitHub, I’ll be very grateful ❤️