#![expect(
clippy::expect_used,
clippy::panic,
reason = "the lint's grant covers `#[test]` functions and `#[cfg(test)]` modules, and a \
helper in an integration-test crate is neither — see AGENTS.md"
)]
use lanekeep_lang::Language;
use lanekeep_lang_js::TypeScript;
use lanekeep_types::{Primitive, Type, TypeScriptOracle, TypeScriptSupport};
use tree_sitter::{Node, Tree};
fn parse(source: &str) -> Tree {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&TypeScript.grammar())
.expect("the TypeScript grammar loads");
parser.parse(source, None).expect("the source parses")
}
fn nodes(tree: &Tree) -> Vec<Node<'_>> {
let mut out = Vec::new();
let mut stack = vec![tree.root_node()];
while let Some(node) = stack.pop() {
out.push(node);
let mut cursor = node.walk();
let children: Vec<Node<'_>> = node.children(&mut cursor).collect();
stack.extend(children.into_iter().rev());
}
out
}
fn last_of<'t>(tree: &'t Tree, kind: &str) -> Node<'t> {
nodes(tree)
.into_iter()
.rfind(|node| node.kind() == kind)
.unwrap_or_else(|| panic!("no `{kind}` node in the tree"))
}
fn type_of_last(source: &str, kind: &str) -> Option<Type> {
let tree = parse(source);
let support = TypeScriptSupport::probe(&TypeScript).expect("TypeScript is supported");
let oracle = TypeScriptOracle::new(&support, &tree, source);
oracle.type_of(last_of(&tree, kind))
}
#[test]
fn a_number_literal_is_a_number() {
assert_eq!(
type_of_last("const x = 42;", "number"),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn a_bigint_literal_is_a_bigint_despite_parsing_as_a_number() {
assert_eq!(
type_of_last("const x = 42n;", "number"),
Some(Type::Primitive(Primitive::BigInt))
);
}
#[test]
fn a_string_literal_is_a_string() {
assert_eq!(
type_of_last("const x = 'a';", "string"),
Some(Type::Primitive(Primitive::String))
);
}
#[test]
fn a_template_string_is_a_string() {
assert_eq!(
type_of_last("const x = `a${b}`;", "template_string"),
Some(Type::Primitive(Primitive::String))
);
}
#[test]
fn the_boolean_literals_are_booleans() {
assert_eq!(
type_of_last("const x = true;", "true"),
Some(Type::Primitive(Primitive::Boolean))
);
assert_eq!(
type_of_last("const x = false;", "false"),
Some(Type::Primitive(Primitive::Boolean))
);
}
#[test]
fn null_and_undefined_are_their_own_primitives() {
assert_eq!(
type_of_last("const x = null;", "null"),
Some(Type::Primitive(Primitive::Null))
);
assert_eq!(
type_of_last("const x = undefined;", "undefined"),
Some(Type::Primitive(Primitive::Undefined))
);
}
#[test]
fn a_parenthesized_expression_is_its_inner_expression() {
assert_eq!(
type_of_last("const x = (42);", "parenthesized_expression"),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn a_grammar_that_speaks_typescript_yields_support() {
assert!(TypeScriptSupport::probe(&TypeScript).is_some());
}
#[test]
fn a_grammar_that_does_not_speak_typescript_yields_no_support() {
assert!(TypeScriptSupport::probe(&lanekeep_lang_python::Python).is_none());
}
#[test]
fn one_probe_serves_many_files() {
let support = TypeScriptSupport::probe(&TypeScript).expect("TypeScript is supported");
for (source, kind, expected) in [
("const a = 1;", "number", Type::Primitive(Primitive::Number)),
(
"const b = 'x';",
"string",
Type::Primitive(Primitive::String),
),
(
"const c = 1n;",
"number",
Type::Primitive(Primitive::BigInt),
),
] {
let tree = parse(source);
let oracle = TypeScriptOracle::new(&support, &tree, source);
assert_eq!(
oracle.type_of(last_of(&tree, kind)),
Some(expected),
"{source}"
);
}
}
#[test]
fn arithmetic_on_numbers_is_a_number() {
assert_eq!(
type_of_last("const x = 1 * 2;", "binary_expression"),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn arithmetic_on_bigints_is_a_bigint() {
assert_eq!(
type_of_last("const x = 2n * 3n;", "binary_expression"),
Some(Type::Primitive(Primitive::BigInt))
);
}
#[test]
fn arithmetic_with_an_operand_the_oracle_cannot_type_is_not_a_number() {
assert_eq!(
type_of_last(
"import { total } from './m';\nconst z = total * 2n;",
"binary_expression"
),
None
);
assert_eq!(
type_of_last(
"class D {}\nconst z = new D() * new D();",
"binary_expression"
),
None
);
}
#[test]
fn a_comparison_is_a_boolean() {
assert_eq!(
type_of_last("const x = 1 < 2;", "binary_expression"),
Some(Type::Primitive(Primitive::Boolean))
);
}
#[test]
fn concatenation_with_a_string_is_a_string() {
assert_eq!(
type_of_last("const x = 'a' + 1;", "binary_expression"),
Some(Type::Primitive(Primitive::String))
);
}
#[test]
fn mixing_a_number_and_a_bigint_is_not_typed() {
assert_eq!(type_of_last("const x = 1 + 1n;", "binary_expression"), None);
}
#[test]
fn an_operator_outside_the_table_is_not_typed() {
assert_eq!(
type_of_expr(
"function f(a: number, b: number) { return a && b; }",
"a && b"
),
None
);
}
#[test]
fn a_nullish_default_over_a_nullable_left_is_the_shared_primitive() {
assert_eq!(
type_of_expr(
"function f(count: number | undefined) { return count ?? 0; }",
"count ?? 0"
),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn a_nullish_default_drops_null_from_the_left() {
assert_eq!(
type_of_expr(
"function f(s: string | null) { return s ?? ''; }",
"s ?? ''"
),
Some(Type::Primitive(Primitive::String))
);
}
#[test]
fn a_nullish_default_over_a_non_nullable_left_is_that_primitive() {
assert_eq!(
type_of_expr(
"function f(a: string, b: string) { return a ?? b; }",
"a ?? b"
),
Some(Type::Primitive(Primitive::String))
);
}
#[test]
fn a_nullish_default_over_a_typed_property_is_that_property_type() {
assert_eq!(
type_of_expr(
"interface Tx { amount: bigint }\nfunction f(tx: Tx) { return tx.amount ?? 0n; }",
"tx.amount ?? 0n"
),
Some(Type::Primitive(Primitive::BigInt))
);
}
#[test]
fn a_nullish_default_over_an_optional_property_is_that_property_type() {
assert_eq!(
type_of_expr(
"interface Tx { amount?: bigint }\nfunction f(tx: Tx) { return tx.amount ?? 0n; }",
"tx.amount ?? 0n"
),
Some(Type::Primitive(Primitive::BigInt))
);
}
#[test]
fn a_nullish_default_with_a_disagreeing_fallback_is_not_typed() {
assert_eq!(
type_of_expr(
"function f(count: number | undefined) { return count ?? 0n; }",
"count ?? 0n"
),
None
);
}
#[test]
fn a_nullish_default_over_a_multi_primitive_left_is_not_typed() {
assert_eq!(
type_of_expr(
"function f(x: number | string | undefined) { return x ?? 0; }",
"x ?? 0"
),
None
);
}
#[test]
fn a_nullish_default_over_a_left_that_reduces_to_a_nominal_is_not_typed() {
assert_eq!(
type_of_expr(
"interface Decimal { c: number }\n\
function f(x: number | Decimal) { return x ?? 0; }",
"x ?? 0"
),
None
);
}
#[test]
fn a_chain_of_nullish_defaults_composes_to_the_shared_primitive() {
assert_eq!(
type_of_expr(
"function f(a: number | undefined, b: number) { return a ?? b ?? 0; }",
"a ?? b ?? 0"
),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn a_nullish_default_with_a_nullable_fallback_is_not_typed() {
assert_eq!(
type_of_expr(
"function f(a: number | undefined, b: number | undefined) { return a ?? b; }",
"a ?? b"
),
None
);
}
#[test]
fn a_nullish_default_over_two_untyped_sides_is_not_typed() {
assert_eq!(type_of_expr("const x = a ?? b;", "a ?? b"), None);
}
#[test]
fn typeof_is_a_string() {
assert_eq!(
type_of_last("const x = typeof y;", "unary_expression"),
Some(Type::Primitive(Primitive::String))
);
}
#[test]
fn a_builtin_conversion_has_the_type_it_converts_to() {
assert_eq!(
type_of_last("const x = parseFloat(s);", "call_expression"),
Some(Type::Primitive(Primitive::Number))
);
assert_eq!(
type_of_last("const x = String(v);", "call_expression"),
Some(Type::Primitive(Primitive::String))
);
}
#[test]
fn a_shadowed_builtin_is_not_typed_by_the_builtin_table() {
assert_eq!(
type_of_last(
"function parseFloat(s: string) { return s; }\nconst x = parseFloat('1');",
"call_expression"
),
None
);
}
#[test]
fn a_call_to_an_ordinary_function_is_not_typed() {
assert_eq!(
type_of_last("const x = myHelper(1);", "call_expression"),
None
);
}
#[test]
fn each_predefined_type_annotation_is_its_primitive() {
for (written, expected) in [
("number", Primitive::Number),
("string", Primitive::String),
("boolean", Primitive::Boolean),
("bigint", Primitive::BigInt),
("symbol", Primitive::Symbol),
] {
assert_eq!(
type_of_last(&format!("let x: {written};"), "type_annotation"),
Some(Type::Primitive(expected)),
"{written}"
);
}
}
#[test]
fn any_and_unknown_are_not_types_the_oracle_will_assert() {
assert_eq!(type_of_last("let x: any;", "type_annotation"), None);
assert_eq!(type_of_last("let x: unknown;", "type_annotation"), None);
}
#[test]
fn a_union_annotation_is_a_union_of_its_members() {
let Some(Type::Union(members)) = type_of_last("let x: number | string;", "type_annotation")
else {
panic!("a two-member union");
};
assert_eq!(
members,
vec![
Type::Primitive(Primitive::Number),
Type::Primitive(Primitive::String),
]
);
}
#[test]
fn a_union_annotation_does_not_depend_on_the_order_written() {
assert_eq!(
type_of_last("let x: number | string;", "type_annotation"),
type_of_last("let x: string | number;", "type_annotation")
);
}
#[test]
fn a_union_with_a_member_the_oracle_cannot_type_is_not_typed_at_all() {
assert_eq!(
type_of_last("let x: number | Foo<T>;", "type_annotation"),
None
);
assert_eq!(
type_of_last("let x: number | (string | boolean);", "type_annotation"),
None
);
assert_eq!(
type_of_last("let x: number[] | string;", "type_annotation"),
None
);
}
#[test]
fn a_comment_inside_a_union_is_not_a_member_of_it() {
assert_eq!(
type_of_last("let x: number /* which one */ | string;", "type_annotation"),
type_of_last("let x: number | string;", "type_annotation")
);
assert!(matches!(
type_of_last("let x: number /* which one */ | string;", "type_annotation"),
Some(Type::Union(_))
));
}
#[test]
fn a_literal_type_takes_its_literal_primitive() {
assert_eq!(
type_of_last("let x: 42;", "type_annotation"),
Some(Type::Primitive(Primitive::Number))
);
assert_eq!(
type_of_last("let x: 'a';", "type_annotation"),
Some(Type::Primitive(Primitive::String))
);
}
#[test]
fn a_named_type_is_nominal() {
assert_eq!(
type_of_last(
"import { Decimal } from 'decimal.js';\nlet x: Decimal;",
"type_annotation"
),
Some(Type::Nominal {
name: "Decimal".to_owned(),
symbol: Some(lanekeep_types::Symbol {
name: "Decimal".to_owned(),
exported: Some("Decimal".to_owned()),
module: Some("decimal.js".to_owned()),
}),
})
);
}
#[test]
fn a_renamed_import_s_symbol_name_is_the_local_alias() {
assert_eq!(
type_of_last(
"import { Decimal as Money } from 'decimal.js';\nlet x: Money;",
"type_annotation"
),
Some(Type::Nominal {
name: "Money".to_owned(),
symbol: Some(lanekeep_types::Symbol {
name: "Money".to_owned(),
exported: Some("Decimal".to_owned()),
module: Some("decimal.js".to_owned()),
}),
})
);
}
#[test]
fn a_locally_declared_type_is_nominal_with_no_module() {
assert_eq!(
type_of_last("class Decimal {}\nlet x: Decimal;", "type_annotation"),
Some(Type::Nominal {
name: "Decimal".to_owned(),
symbol: Some(lanekeep_types::Symbol {
name: "Decimal".to_owned(),
exported: None,
module: None,
}),
})
);
}
#[test]
fn an_ambient_type_is_nominal_with_no_symbol() {
assert_eq!(
type_of_last("let x: Date;", "type_annotation"),
Some(Type::Nominal {
name: "Date".to_owned(),
symbol: None,
})
);
}
#[test]
fn a_function_type_annotation_is_not_typed() {
assert_eq!(
type_of_last("let x: () => number;", "type_annotation"),
None
);
}
#[test]
fn a_locally_declared_bigint_shadows_the_primitive() {
assert_eq!(
type_of_last("class bigint {}\nlet x: bigint;", "type_annotation"),
Some(Type::Nominal {
name: "bigint".to_owned(),
symbol: Some(lanekeep_types::Symbol {
name: "bigint".to_owned(),
exported: None,
module: None,
}),
})
);
}
#[test]
fn a_locally_aliased_bigint_resolves_through_the_alias() {
assert_eq!(
type_of_last("type bigint = string;\nlet x: bigint;", "type_annotation"),
Some(Type::Primitive(Primitive::String))
);
}
fn type_of_use(source: &str, name: &str) -> Option<Type> {
let tree = parse(source);
let support = TypeScriptSupport::probe(&TypeScript).expect("TypeScript is supported");
let oracle = TypeScriptOracle::new(&support, &tree, source);
let found = nodes(&tree)
.into_iter()
.rfind(|node| node.kind() == "identifier" && source.get(node.byte_range()) == Some(name));
oracle.type_of(found.unwrap_or_else(|| panic!("no use of `{name}`")))
}
#[test]
fn an_annotated_parameter_has_its_annotated_type() {
assert_eq!(
type_of_use(
"function credit(amount: number) { return amount; }",
"amount"
),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn an_annotated_optional_parameter_has_its_annotated_type() {
assert_eq!(
type_of_use(
"function credit(amount?: number) { return amount; }",
"amount"
),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn a_destructured_parameter_is_not_given_its_pattern_s_type() {
for source in [
"function credit({ rate }: Money) { return rate; }",
"function credit({ rate }?: Money) { return rate; }",
"function credit([rate]: Money) { return rate; }",
"const credit = ({ rate }: Money) => rate;",
] {
assert_eq!(type_of_use(source, "rate"), None, "{source}");
}
}
#[test]
fn the_pattern_guard_leaves_a_plain_binding_alone() {
assert_eq!(
type_of_use(
"function credit(amount: number) { return amount; }",
"amount"
),
Some(Type::Primitive(Primitive::Number))
);
assert_eq!(
type_of_use("let amount!: number;\nconst y = amount;", "amount"),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn a_destructured_local_is_not_given_its_initializer_or_annotation_type() {
assert_eq!(
type_of_use(
"const s = String(q);\nconst { length } = s;\nconst y = length;",
"length"
),
None
);
assert_eq!(
type_of_use("const { rate }: Money = order;\nconst y = rate;", "rate"),
None
);
assert_eq!(
type_of_use("const [first] = xs;\nconst y = first;", "first"),
None
);
}
#[test]
fn a_local_takes_the_type_of_its_initializer() {
assert_eq!(
type_of_use(
"const amount = parseFloat(raw);\nconst y = amount;",
"amount"
),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn an_annotation_beats_the_initializer_it_sits_beside() {
assert_eq!(
type_of_use(
"const amount: string = parseFloat(raw);\nconst y = amount;",
"amount"
),
Some(Type::Primitive(Primitive::String))
);
}
#[test]
fn a_local_annotated_with_a_named_type_is_nominal() {
assert_eq!(
type_of_use(
"import { Decimal } from 'decimal.js';\nfunction f(x: Decimal) { return x; }",
"x"
),
Some(Type::Nominal {
name: "Decimal".to_owned(),
symbol: Some(lanekeep_types::Symbol {
name: "Decimal".to_owned(),
exported: Some("Decimal".to_owned()),
module: Some("decimal.js".to_owned()),
}),
})
);
}
#[test]
fn an_imported_value_has_no_type_yet() {
assert_eq!(
type_of_use("import { total } from './m';\nconst y = total;", "total"),
None
);
}
#[test]
fn an_undeclared_name_has_no_type() {
assert_eq!(type_of_use("const y = missing;", "missing"), None);
}
#[test]
fn a_chain_of_initializers_terminates() {
let source = "const a = b;\nconst b = a;\nconst c = a;\n";
assert_eq!(type_of_use(source, "c"), None);
}
#[test]
fn a_same_file_type_alias_resolves_to_what_it_aliases() {
assert_eq!(
type_of_last("type Amount = number;\nlet x: Amount;", "type_annotation"),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn an_alias_chain_resolves_through_every_link() {
assert_eq!(
type_of_last(
"type A = number;\ntype B = A;\ntype C = B;\nlet x: C;",
"type_annotation"
),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn an_alias_cycle_terminates_without_an_answer() {
assert_eq!(
type_of_last("type A = B;\ntype B = A;\nlet x: A;", "type_annotation"),
None
);
}
#[test]
fn an_alias_to_an_untyped_type_is_untyped() {
assert_eq!(
type_of_last("type A = () => void;\nlet x: A;", "type_annotation"),
None
);
}
#[test]
fn a_type_parameter_is_not_answered_by_an_outer_alias_of_the_same_name() {
for source in [
"type A = number;\nfunction f<A>(x: A) { return x; }",
"type A = number;\nclass C<A> { m(x: A) { return x; } }",
"type A = number;\nclass C { m<A>(x: A) { return x; } }",
"type A = number;\nconst f = <A,>(x: A) => x;",
] {
assert_eq!(type_of_use(source, "x"), None, "{source}");
}
}
#[test]
fn an_alias_still_answers_a_parameter_that_no_type_parameter_shadows() {
assert_eq!(
type_of_use("type A = number;\nfunction f(x: A) { return x; }", "x"),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn a_for_of_loop_variable_is_not_answered_by_the_binding_it_shadows() {
for source in [
"const x = 'a';\nfor (const x of ns) { g(x); }",
"const x = 'a';\nfor (let x of ns) { g(x); }",
"const x = 'a';\nfor (const x in ns) { g(x); }",
] {
assert_eq!(type_of_use(source, "x"), None, "{source}");
}
}
#[test]
fn a_loop_head_that_declares_nothing_leaves_the_outer_binding_reachable() {
assert_eq!(
type_of_use("const x = 'a';\nfor (x of ns) { g(x); }", "x"),
Some(Type::Primitive(Primitive::String))
);
assert_eq!(
type_of_use("const x = 'a';\nfor (const y of ns) { g(x); }", "x"),
Some(Type::Primitive(Primitive::String))
);
}
#[test]
fn the_debug_impl_does_not_print_the_whole_source() {
let source = "const aNameThatMustNotReachALogLine = 1;";
let tree = parse(source);
let support = TypeScriptSupport::probe(&TypeScript).expect("TypeScript is supported");
let oracle = TypeScriptOracle::new(&support, &tree, source);
let rendered = format!("{oracle:?}");
assert!(
!rendered.contains("aNameThatMustNotReachALogLine"),
"{rendered}"
);
assert!(rendered.contains("source_len"), "{rendered}");
}
fn symbol_of_use(source: &str, name: &str) -> Option<lanekeep_types::Symbol> {
let tree = parse(source);
let support = TypeScriptSupport::probe(&TypeScript).expect("TypeScript is supported");
let oracle = TypeScriptOracle::new(&support, &tree, source);
let found = nodes(&tree).into_iter().rfind(|node| {
matches!(node.kind(), "identifier" | "type_identifier")
&& source.get(node.byte_range()) == Some(name)
});
oracle.symbol_of(found.unwrap_or_else(|| panic!("no use of `{name}`")))
}
#[test]
fn an_imported_name_carries_the_module_it_came_from() {
assert_eq!(
symbol_of_use(
"import { Decimal } from 'decimal.js';\nconst x = Decimal;",
"Decimal"
),
Some(lanekeep_types::Symbol {
name: "Decimal".to_owned(),
exported: Some("Decimal".to_owned()),
module: Some("decimal.js".to_owned()),
})
);
}
#[test]
fn a_locally_declared_name_carries_no_module() {
assert_eq!(
symbol_of_use("class Decimal {}\nconst x = Decimal;", "Decimal"),
Some(lanekeep_types::Symbol {
name: "Decimal".to_owned(),
exported: None,
module: None,
})
);
}
#[test]
fn a_name_nothing_declares_has_no_symbol() {
assert_eq!(symbol_of_use("const x = missing;", "missing"), None);
}
#[test]
fn a_renamed_import_carries_the_exported_name_beside_the_alias() {
assert_eq!(
symbol_of_use(
"import { Decimal as Money } from 'decimal.js';\nconst x = Money;",
"Money"
),
Some(lanekeep_types::Symbol {
name: "Money".to_owned(),
exported: Some("Decimal".to_owned()),
module: Some("decimal.js".to_owned()),
})
);
}
#[test]
fn a_default_import_is_exported_under_the_name_default() {
assert_eq!(
symbol_of_use("import Money from 'decimal.js';\nconst x = Money;", "Money"),
Some(lanekeep_types::Symbol {
name: "Money".to_owned(),
exported: Some("default".to_owned()),
module: Some("decimal.js".to_owned()),
})
);
}
#[test]
fn a_namespace_import_has_a_module_and_no_exported_name() {
assert_eq!(
symbol_of_use("import * as d from 'decimal.js';\nconst x = d;", "d"),
Some(lanekeep_types::Symbol {
name: "d".to_owned(),
exported: None,
module: Some("decimal.js".to_owned()),
})
);
}
#[test]
fn a_string_named_import_specifier_is_exported_without_its_quotes() {
assert_eq!(
symbol_of_use(
"import { \"Decimal\" as D } from 'decimal.js';\nconst x = D;",
"D"
),
Some(lanekeep_types::Symbol {
name: "D".to_owned(),
exported: Some("Decimal".to_owned()),
module: Some("decimal.js".to_owned()),
})
);
}
#[test]
fn two_runs_over_one_input_agree() {
let source = "import { Decimal } from 'decimal.js';\n\
type Amount = number | string;\n\
function f(a: Amount, b: Decimal) { const c = parseFloat('1'); return c; }\n";
let first = format!("{:?}", type_of_use(source, "c"));
let second = format!("{:?}", type_of_use(source, "c"));
assert_eq!(first, second);
let one = format!("{:?}", type_of_last(source, "union_type"));
let other = format!("{:?}", type_of_last(source, "union_type"));
assert_eq!(one, other);
}
#[test]
fn a_type_parameter_on_any_declaration_kind_gives_nothing() {
for source in [
"interface O<T> { x: T }",
"type O<T> = { x: T };",
"abstract class C<T> { abstract x: T }",
"declare function f<T>(x: T): T;",
] {
assert_eq!(type_of_last(source, "type_annotation"), None, "{source}");
}
}
#[test]
fn a_type_parameter_shadowing_an_alias_does_not_answer_with_the_alias() {
for source in [
"type A = number;\ninterface O<A> { x: A }",
"type A = number;\ntype O<A> = { x: A };",
"type A = number;\nabstract class C<A> { abstract x: A }",
"type A = number;\ndeclare function f<A>(x: A): A;",
] {
assert_eq!(type_of_last(source, "type_annotation"), None, "{source}");
}
}
#[test]
fn without_a_type_parameter_a_member_still_reads_the_outer_alias() {
for source in [
"type A = number;\ninterface O { x: A }",
"type A = number;\ntype O = { x: A };",
"type A = number;\nabstract class C { abstract x: A }",
] {
assert_eq!(
type_of_last(source, "type_annotation"),
Some(Type::Primitive(Primitive::Number)),
"{source}"
);
}
}
#[test]
fn an_ambient_functions_parameter_is_typed() {
assert_eq!(
type_of_last("declare function f(a: number): void;", "identifier"),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn a_type_parameter_on_a_method_signature_kind_gives_nothing() {
for source in [
"type A = number;\ninterface I { m<A>(x: A): A }",
"type A = number;\nabstract class C { abstract m<A>(x: A): A }",
] {
assert_eq!(type_of_last(source, "type_annotation"), None, "{source}");
}
}
#[test]
fn without_a_type_parameter_a_method_signature_kind_reads_the_alias() {
for source in [
"type A = number;\ninterface I { m(x: A): A }",
"type A = number;\nabstract class C { abstract m(x: A): A }",
] {
assert_eq!(
type_of_last(source, "type_annotation"),
Some(Type::Primitive(Primitive::Number)),
"{source}"
);
}
}
#[test]
fn a_parameter_of_a_method_signature_kind_is_typed() {
for source in [
"interface I { m(a: number): void }",
"abstract class C { abstract m(a: number): void }",
] {
assert_eq!(
type_of_use(source, "a"),
Some(Type::Primitive(Primitive::Number)),
"{source}"
);
}
}
#[test]
fn a_type_parameter_in_signature_or_type_position_gives_nothing() {
for source in [
"type A = number;\ninterface F { <A>(x: A): A }",
"type A = number;\ninterface F { new <A>(x: A): A }",
"type A = number;\ntype F = new <A>(x: A) => A;",
"type A = number;\ntype F = <A>(x: A) => A;",
] {
assert_eq!(type_of_last(source, "type_annotation"), None, "{source}");
}
}
#[test]
fn without_a_type_parameter_signature_or_type_position_reads_the_alias() {
for source in [
"type A = number;\ninterface F { (x: A): A }",
"type A = number;\ninterface F { new (x: A): A }",
"type A = number;\ntype F = new (x: A) => A;",
"type A = number;\ntype F = (x: A) => A;",
] {
assert_eq!(
type_of_last(source, "type_annotation"),
Some(Type::Primitive(Primitive::Number)),
"{source}"
);
}
}
#[test]
fn a_parameter_in_signature_or_type_position_is_typed() {
for source in [
"interface F { (a: number): void }",
"interface F { new (a: number): F }",
"type F = new (a: number) => F;",
"type F = (a: number) => void;",
] {
assert_eq!(
type_of_use(source, "a"),
Some(Type::Primitive(Primitive::Number)),
"{source}"
);
}
}
#[test]
fn an_oracle_with_no_import_resolution_answers_nothing_for_an_import() {
assert_eq!(
type_of_use(
"import { Decimal } from 'm';\nconst y = Decimal;",
"Decimal"
),
None
);
}
fn type_of_expr(source: &str, text: &str) -> Option<Type> {
let tree = parse(source);
let support = TypeScriptSupport::probe(&TypeScript).expect("TypeScript is supported");
let oracle = TypeScriptOracle::new(&support, &tree, source);
let found = nodes(&tree)
.into_iter()
.find(|node| source.get(node.byte_range()) == Some(text));
oracle.type_of(found.unwrap_or_else(|| panic!("no node with text `{text}`")))
}
#[test]
fn a_member_off_a_same_file_interface_is_its_annotated_type() {
assert_eq!(
type_of_expr(
"interface Order { amount: number }\nfunction f(o: Order) { return o.amount; }",
"o.amount"
),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn a_member_off_a_same_file_object_alias_is_its_annotated_type() {
assert_eq!(
type_of_expr(
"type Order = { amount: bigint };\nfunction f(o: Order) { return o.amount; }",
"o.amount"
),
Some(Type::Primitive(Primitive::BigInt))
);
}
#[test]
fn a_field_off_a_same_file_class_is_its_annotated_type() {
assert_eq!(
type_of_expr(
"class Order { amount: number = 0 }\nfunction f(o: Order) { return o.amount; }",
"o.amount"
),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn an_unknown_member_answers_nothing() {
assert_eq!(
type_of_expr(
"interface Order { amount: number }\nfunction f(o: Order) { return o.missing; }",
"o.missing"
),
None
);
}
#[test]
fn a_member_off_an_untyped_base_answers_nothing() {
assert_eq!(
type_of_expr("function f(o) { return o.amount; }", "o.amount"),
None
);
}
#[test]
fn an_optional_access_is_the_member_type_or_undefined() {
assert_eq!(
type_of_expr(
"interface Order { amount: number }\nfunction f(o: Order) { return o?.amount; }",
"o?.amount"
),
Type::union(vec![
Type::Primitive(Primitive::Number),
Type::Primitive(Primitive::Undefined),
])
);
}
#[test]
fn an_optional_member_is_the_member_type_or_undefined() {
assert_eq!(
type_of_expr(
"interface Order { amount?: number }\nfunction f(o: Order) { return o.amount; }",
"o.amount"
),
Type::union(vec![
Type::Primitive(Primitive::Number),
Type::Primitive(Primitive::Undefined),
])
);
}
#[test]
fn a_chain_reads_through_same_file_types() {
assert_eq!(
type_of_expr(
"interface Inner { amount: number }\ninterface Outer { inner: Inner }\n\
function f(o: Outer) { return o.inner.amount; }",
"o.inner.amount"
),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn an_optional_link_taints_the_rest_of_the_chain() {
assert_eq!(
type_of_expr(
"interface Inner { amount: number }\ninterface Outer { inner: Inner }\n\
function f(o: Outer) { return o?.inner.amount; }",
"o?.inner.amount"
),
Type::union(vec![
Type::Primitive(Primitive::Number),
Type::Primitive(Primitive::Undefined),
])
);
}
#[test]
fn a_string_literal_subscript_reads_a_member() {
assert_eq!(
type_of_expr(
"interface Order { amount: number }\nfunction f(o: Order) { return o[\"amount\"]; }",
"o[\"amount\"]"
),
Some(Type::Primitive(Primitive::Number))
);
}
#[test]
fn a_dynamic_subscript_answers_nothing() {
assert_eq!(
type_of_expr(
"interface Order { amount: number }\nfunction f(o: Order, k: string) { return o[k]; }",
"o[k]"
),
None
);
}
#[test]
fn a_member_off_a_shadowed_type_reads_the_shadow() {
assert_eq!(
type_of_expr(
"interface Box { value: number }\n\
function f() {\n\
\x20 type Box = { value: string };\n\
\x20 const b: Box = { value: 's' };\n\
\x20 return b.value;\n\
}",
"b.value"
),
Some(Type::Primitive(Primitive::String))
);
}
#[test]
fn a_member_off_a_nullable_receiver_is_or_undefined() {
assert_eq!(
type_of_expr(
"interface Order { amount: number }\n\
function f(o: Order | undefined) { return o.amount; }",
"o.amount"
),
Type::union(vec![
Type::Primitive(Primitive::Number),
Type::Primitive(Primitive::Undefined),
])
);
}
#[test]
fn a_nullable_intermediate_member_taints_the_tail() {
assert_eq!(
type_of_expr(
"interface Amount { cents: number }\n\
interface Order { amount: Amount | null }\n\
function f(o: Order) { return o.amount.cents; }",
"o.amount.cents"
),
Type::union(vec![
Type::Primitive(Primitive::Number),
Type::Primitive(Primitive::Undefined),
])
);
}
#[test]
fn a_chain_reads_through_an_inline_object_member() {
assert_eq!(
type_of_expr(
"interface Outer { inner: { amount: number } }\n\
function f(o: Outer) { return o.inner.amount; }",
"o.inner.amount"
),
Some(Type::Primitive(Primitive::Number))
);
}