| name | type-inference |
| description | 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. |
| compatibility | Designed for coding agents working on the Biome codebase (github.com/biomejs/biome). |
Purpose
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.
Prerequisites
- Read
crates/biome_js_type_info/CONTRIBUTING.md for architecture details
- Understand Biome's focus on IDE support and instant updates
- Familiarity with TypeScript type system concepts
Code Standards
CRITICAL: No Emojis
Emojis are BANNED in all type inference code:
- NO emojis in code comments
- NO emojis in rustdoc documentation
- NO emojis in test files
- NO emojis in debug output or error messages
Keep all code professional and emoji-free.
Key Concepts
Module Graph Constraint
Critical 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.
Type Data Structure
Types are stored in TypeData enum with many variants:
enum TypeData {
Unknown,
Global,
BigInt, Boolean, Null, Number,
String, Symbol, Undefined,
Function(Box<Function>),
Object(Box<Object>),
Class(Box<Class>),
Interface(Box<Interface>),
Union(Box<Union>),
Intersection(Box<Intersection>),
Tuple(Box<Tuple>),
Literal(Box<Literal>),
Reference(TypeReference),
TypeofExpression(Box<TypeofExpression>),
}
Type References
Instead of direct type references, use TypeReference:
enum TypeReference {
Qualifier(Box<TypeReferenceQualifier>),
Resolved(ResolvedTypeId),
Import(Box<TypeImportQualifier>),
}
Note: There is no Unknown variant. Unknown types are represented as TypeReference::Resolved(GLOBAL_UNKNOWN_ID). Use TypeReference::unknown() to create one.
Type Resolution Phases
1. Local Inference
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
2. Module-Level ("Thin") Inference
What: Resolves references within a single module's scope.
Process:
- Takes results from local inference
- Looks up qualifiers in local scopes
- Converts to
TypeReference::Resolved if found locally
- Converts to
TypeReference::Import if from import statement
- Falls back to globals (like
Array, Promise)
- Uses
TypeReference::Unknown if nothing found
Where: Implemented in js_module_info/collector.rs
Output: Types with resolved local references, import markers, or unknown
3. Full Inference
What: Resolves import references across module boundaries.
Process:
- Has access to entire module graph
- Resolves
TypeReference::Import by following imports
- Converts to
TypeReference::Resolved after following imports
Where: Implemented in js_module_info/module_resolver.rs
Limitation: Results cannot be cached (would become stale on file changes)
Working with Type Resolvers
Available Resolvers
HardcodedSymbolResolver
GlobalsResolver
JsModuleInfoCollector
ModuleResolver
Using a Resolver
use biome_js_type_info::{TypeResolver, ResolvedTypeData};
fn analyze_type(resolver: &impl TypeResolver, type_ref: TypeReference) {
let resolved_data: ResolvedTypeData = resolver.resolve_type(type_ref);
match resolved_data.as_raw_data() {
TypeData::String => { },
TypeData::Number => { },
TypeData::Function(func) => { },
_ => { }
}
if let TypeData::Reference(inner_ref) = resolved_data.as_raw_data() {
let inner_data = resolver.resolve_type(*inner_ref);
}
}
Type Flattening
What: Converts complex type expressions to concrete types.
Example: After resolving a + b:
- If both are
TypeData::Number → Flatten to TypeData::Number
- Otherwise → Usually flatten to
TypeData::String
Where: Implemented in flattening.rs
Common Workflows
Implement Type-Aware Lint Rule
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();
let resolver = model.type_resolver();
let expr_type = node.callee().ok()?.infer_type(resolver);
match expr_type.as_raw_data() {
TypeData::Function(_) => { },
TypeData::Unknown => { },
_ => { return Some(()); }
}
None
}
}
Navigate Type References
fn is_string_type(resolver: &impl TypeResolver, type_ref: TypeReference) -> bool {
let resolved = resolver.resolve_type(type_ref);
let data = match resolved.as_raw_data() {
TypeData::Reference(ref_to) => resolver.resolve_type(*ref_to),
_other => resolved,
};
matches!(data.as_raw_data(), TypeData::String)
}
Work with Function Types
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() {
for param in func_type.parameters() {
let param_type = resolver.resolve_type(param.type_ref());
}
let return_type = resolver.resolve_type(func_type.return_type());
}
}
Architecture Principles
Why Type References?
Advantages:
- No stale data: Module updates don't leave old types in memory
- Better performance: Types stored in vectors (data locality)
- Easier debugging: Can inspect all types in vector
- Simpler algorithms: Process vectors instead of traversing graphs
Trade-off: Must explicitly resolve references (not automatic like Arc)
ResolvedTypeId Structure
struct ResolvedTypeId(ResolverId, TypeId)
TypeId (u32): Index into a type vector
ResolverId (u32): Identifies which vector to use
- Total: 64 bits (compact representation)
ResolvedTypeData
Always work with ResolvedTypeData from resolver, not raw &TypeData:
let resolved_data: ResolvedTypeData = resolver.resolve_type(type_ref);
let raw_data: &TypeData = resolved_data.as_raw_data();
Tips
- Unknown types:
TypeData::Unknown means inference not implemented, treat as "could be anything"
- Follow references: Always follow
TypeData::Reference to get actual type
- Resolver context: Keep
ResolvedTypeData when possible, don't extract raw TypeData early
- Performance: Type vectors are fast - iterate directly instead of recursive traversal
- IDE focus: All design decisions prioritize instant IDE updates over CLI performance
- No caching: Full inference results can't be cached (would become stale)
- Globals: Currently hardcoded, eventually should use TypeScript's
.d.ts files
Common Patterns
let type_ref = expr.infer_type(resolver);
let flattened = type_ref.flatten(resolver);
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)
}
match resolved.as_raw_data() {
TypeData::Unknown | TypeData::UnknownKeyword => {
return None;
}
TypeData::String => { }
_ => { }
}
CSS and HTML Module Graph
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.
Key Types
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 Design
CssClass stores a class name without allocating a String per word:
pub struct CssClass {
pub(crate) token: TokenText,
pub range: TextRange,
}
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.
- For CSS selectors (
.foo), the token is the whole selector token and the range covers it entirely.
- For HTML/JSX string attributes (
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.
Populating CssClass from a CSS selector
let 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)),
});
Populating CssClass from a class="foo bar" attribute
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(),
range: TextRange::new(start, end),
});
offset = word_offset + word.len() as u32;
}
Cross-file class lookup
module_graph.is_class_referenced_by_importers(css_file_path, class_name_str)
let html_info = module_graph.html_module_info_for_path(file_path)?;
let css_info = module_graph.css_module_info_for_path(stylesheet_path)?;
html_info.style_classes.contains("foo")
css_info.classes.contains("bar")
Public Function Audit Rules
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:
- A function used only in tests is not justified — remove it
- Tests calling a function do not count as "production use"
- Check with
grep across all crates before removing anything
data() is used from biome_service/workspace/server.rs — do not remove it even if it looks test-only
References
- Architecture guide:
crates/biome_js_type_info/CONTRIBUTING.md
- Module graph:
crates/biome_module_graph/
- Type resolver trait:
crates/biome_js_type_info/src/resolver.rs
- Flattening:
crates/biome_js_type_info/src/flattening.rs