Guide for working with Biome's module graph and type inference system. Use when implementing type-aware lint rules, understanding type resolution, working on the module graph infrastructure, or implementing type inference for new features.
68
82%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Use this skill when working with Biome's type inference system and module graph. Covers type references, resolution phases, and the architecture designed for IDE performance.
crates/biome_js_type_info/CONTRIBUTING.md for architecture detailsCritical rule: No module may copy or clone data from another module, not even behind Arc.
Why: Any module can be updated at any time (IDE file changes). Copying data would create stale references that are hard to invalidate.
Solution: Use TypeReference instead of direct type references.
Types are stored in TypeData enum with many variants:
// Simplified — see crates/biome_js_type_info/src/type_data.rs for the full enum
enum TypeData {
Unknown, // Inference not implemented
Global, // Global type reference
BigInt, Boolean, Null, Number, // Primitive types
String, Symbol, Undefined,
Function(Box<Function>), // Function with parameters
Object(Box<Object>), // Object with properties
Class(Box<Class>), // Class definition
Interface(Box<Interface>), // Interface definition
Union(Box<Union>), // Union type (A | B)
Intersection(Box<Intersection>), // Intersection type (A & B)
Tuple(Box<Tuple>), // Tuple type
Literal(Box<Literal>), // Literal type ("foo", 42)
Reference(TypeReference), // Reference to another type
TypeofExpression(Box<TypeofExpression>), // typeof an expression
// ... plus Conditional, Generic, TypeOperator, InstanceOf,
// keyword variants (AnyKeyword, NeverKeyword, VoidKeyword, etc.)
}Instead of direct type references, use TypeReference:
enum TypeReference {
Qualifier(Box<TypeReferenceQualifier>), // Name-based reference
Resolved(ResolvedTypeId), // Resolved to type ID
Import(Box<TypeImportQualifier>), // Import reference
}Note: There is no Unknown variant. Unknown types are represented as TypeReference::Resolved(GLOBAL_UNKNOWN_ID). Use TypeReference::unknown() to create one.
What: Derives types from expressions without surrounding context.
Example: For a + b, creates:
TypeData::TypeofExpression(TypeofExpression::Addition {
left: TypeReference::from(TypeReferenceQualifier::from_name("a")),
right: TypeReference::from(TypeReferenceQualifier::from_name("b"))
})Where: Implemented in local_inference.rs
Output: Types with unresolved TypeReference::Qualifier references
What: Resolves references within a single module's scope.
Process:
TypeReference::Resolved if found locallyTypeReference::Import if from import statementArray, Promise)TypeReference::unknown() if nothing is foundWhere: Implemented in js_module_info/collector.rs
Output: Types with resolved local references, import markers, or unknown
What: Resolves import references across module boundaries.
Process:
TypeReference::Import by following importsTypeReference::Resolved after following importsWhere: The Salsa-backed implementation starts at
db/queries/type_inference.rs::infer_module_types and uses helpers under
db/type_inference/. js_module_info/module_resolver.rs contains the legacy
TypeResolver-based path.
Caching: infer_module_types is tracked by Salsa. Imported module results
are dependencies, so Salsa invalidates affected importers after a change.
// 1. For tests
HardcodedSymbolResolver
// 2. For globals (Array, Promise, etc.)
GlobalsResolver
// 3. For thin inference (single module)
JsModuleInfoCollector
// 4. For full inference (across modules)
ModuleResolveruse biome_js_type_info::{TypeResolver, ResolvedTypeData};
fn analyze_type(resolver: &impl TypeResolver, type_ref: TypeReference) {
// Resolve the reference
let resolved_data: ResolvedTypeData = resolver.resolve_type(type_ref);
// Get raw data for pattern matching
match resolved_data.as_raw_data() {
TypeData::String => { /* handle string */ },
TypeData::Number => { /* handle number */ },
TypeData::Function(func) => { /* handle function */ },
_ => { /* handle others */ }
}
// Resolve nested references
if let TypeData::Reference(inner_ref) = resolved_data.as_raw_data() {
let inner_data = resolver.resolve_type(*inner_ref);
// Process inner type
}
}What: Converts complex type expressions to concrete types.
Example: After resolving a + b:
TypeData::Number → Flatten to TypeData::NumberTypeData::StringWhere: Implemented in flattening.rs
use biome_analyze::Semantic;
use biome_js_type_info::{TypeResolver, TypeData};
impl Rule for MyTypeRule {
type Query = Semantic<JsCallExpression>;
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let model = ctx.model();
// Get type resolver from model
let resolver = model.type_resolver();
// Get type of expression
let expr_type = node.callee().ok()?.infer_type(resolver);
// Check the type
match expr_type.as_raw_data() {
TypeData::Function(_) => { /* valid */ },
TypeData::Unknown => { /* might be valid, can't tell */ },
_ => { return Some(()); /* not callable */ }
}
None
}
}fn is_string_type(resolver: &impl TypeResolver, type_ref: TypeReference) -> bool {
let resolved = resolver.resolve_type(type_ref);
// Follow references
let data = match resolved.as_raw_data() {
TypeData::Reference(ref_to) => resolver.resolve_type(*ref_to),
_other => resolved,
};
// Check the resolved type
matches!(data.as_raw_data(), TypeData::String)
}fn analyze_function(resolver: &impl TypeResolver, type_ref: TypeReference) {
let resolved = resolver.resolve_type(type_ref);
if let TypeData::Function(func_type) = resolved.as_raw_data() {
// Access parameters
for param in func_type.parameters() {
let param_type = resolver.resolve_type(param.type_ref());
// Analyze parameter type
}
// Access return type
let return_type = resolver.resolve_type(func_type.return_type());
}
}Advantages:
Trade-off: Must explicitly resolve references (not automatic like Arc)
struct ResolvedTypeId(ResolverId, TypeId)TypeId (u32): Index into a type vectorResolverId (u32): Identifies which vector to useAlways work with ResolvedTypeData from resolver, not raw &TypeData:
// Good - tracks resolver context
let resolved_data: ResolvedTypeData = resolver.resolve_type(type_ref);
// Be careful - loses resolver context
let raw_data: &TypeData = resolved_data.as_raw_data();
// Can't resolve nested TypeReferences without ResolverId!TypeData::Unknown means inference not implemented, treat as "could be anything"TypeData::Reference to get actual typeResolvedTypeData when possible, don't extract raw TypeData early.d.ts files// Pattern 1: Resolve and flatten
let type_ref = expr.infer_type(resolver);
let flattened = type_ref.flatten(resolver);
// Pattern 2: Check if type matches
fn is_string_type(resolver: &impl TypeResolver, type_ref: TypeReference) -> bool {
let resolved = resolver.resolve_type(type_ref);
matches!(resolved.as_raw_data(), TypeData::String)
}
// Pattern 3: Handle unknown gracefully
match resolved.as_raw_data() {
TypeData::Unknown | TypeData::UnknownKeyword => {
// Can't verify, assume valid
return None;
}
TypeData::String => { /* handle */ }
_ => { /* handle */ }
}The module graph tracks not only JS imports/exports but also CSS class names and HTML class references, used by cross-file lint rules like noUnusedStyles and noUndeclaredStyles.
CssModuleInfo — classes: IndexSet<CssClass>
HtmlModuleInfo — style_classes: IndexSet<CssClass> (from <style> blocks)
— referenced_classes: IndexSet<CssClass> (from class="..." attrs)
— imported_stylesheets: Vec<ResolvedPath>
JsModuleInfo — referenced_classes: IndexSet<CssClass> (from className="...")CssClass DesignCssClass stores a class name without allocating a String per word:
pub struct CssClass {
pub(crate) token: TokenText, // the full token (or its inner text) — refcount only
pub range: TextRange, // byte range relative to token.text()
}
impl CssClass {
pub fn text(&self) -> &str {
let start = usize::from(self.range.start());
let end = usize::from(self.range.end());
&self.token.text()[start..end]
}
}Borrow<str>, Hash, and Eq all delegate to self.text(), so IndexSet::contains("foo") works with a plain &str..foo), the token is the whole selector token and the range covers it entirely.class="foo bar"), the token is the inner (quote-stripped) TokenText from inner_string_text(), and each word has its own offset range within that inner text.CssClass from a CSS selectorlet token_text = token.token_text_trimmed();
let len = u32::from(token_text.len());
classes.insert(CssClass {
token: token_text,
range: TextRange::new(TextSize::from(0), TextSize::from(len)),
});CssClass from a class="foo bar" attribute// Use inner_string_text() — strips quotes, no allocation.
let inner: TokenText = html_string.inner_string_text()?;
let content = inner.text();
let mut offset: u32 = 0;
for word in content.split_ascii_whitespace() {
let word_offset = content[offset as usize..]
.find(word)
.map_or(offset, |pos| offset + pos as u32);
let start = TextSize::from(word_offset);
let end = start + TextSize::from(word.len() as u32);
classes.insert(CssClass {
token: inner.clone(), // refcount bump only
range: TextRange::new(start, end),
});
offset = word_offset + word.len() as u32;
}// In a CSS lint rule:
module_graph.is_class_referenced_by_importers(css_file_path, class_name_str)
// In an HTML lint rule:
let html_info = module_graph.html_module_info_for_path(file_path)?;
let css_info = module_graph.css_module_info_for_path(stylesheet_path)?;
// Zero-alloc lookup (Borrow<str> impl):
html_info.style_classes.contains("foo")
css_info.classes.contains("bar")When adding or removing functions from the module graph, always verify each public function has a real production call site (not just test code).
Rules:
grep across all crates before removing anythingdata() is used from biome_service/workspace/server.rs — do not remove it even if it looks test-onlycrates/biome_js_type_info/CONTRIBUTING.mdcrates/biome_module_graph/crates/biome_js_type_info/src/resolver.rscrates/biome_js_type_info/src/flattening.rsc50a853
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.