// `mtsc check` — single-file checker diagnostics, and the checker's own
// measurement harness.
//
// mtsc check # parse + check, exits 0
// mtsc check --parse # parse only (compares against the
// # full pipeline so the checker's
// # marginal cost is visible)
// mtsc check --iters N # repeat N times so memory tools see
// # the steady-state heap
//
// This was the `tscheck` binary, and it is NOT the same thing as
// `mtsc --noEmit`. The difference is the program: `--noEmit` walks the
// import graph and checks it as one program, the way `tsc` does, while
// this checks ONE file with a single-file model — which is what the
// conformance corpus wants, since each of its 4,484 cases is a program
// of one file.
//
// Three properties of this output are an interface, not a style:
//
// * the summary line's exact wording. `checker_conformance_oracle.sh`,
// `checker_precision.sh`, `verify_checker_scaling.mjs` and
// `checker_miss_buckets.mjs` all parse it.
// * `-v` means VERBOSE here, where the top-level `mtsc -v` is `tsc`'s
// version flag. The two do not collide because they are in different
// scopes, which is the one thing the verb layout buys for free.
// * it exits 0 whatever it finds, including on a read or parse error.
// It is a measurement tool, and the oracle scripts that drive it
// over thousands of files run under `set -e`.
///|
/// True when `spec`, imported from `file_path`, resolves on disk. Used to
/// suppress "cannot find module" issues for files living under
/// node_modules: single-file checking cannot see sibling packages
/// (`csstype` is a real dependency of @types/react) or relative
/// self-imports (`./` resolves to the package's index.d.ts), and tsc
/// accepts both. Conformance corpora live outside node_modules, so oracle
/// behavior is unchanged.
async fn module_spec_resolves_on_disk(
file_path : String,
spec : String,
) -> Bool {
let dir = match file_path.rev_find("/") {
Some(idx) => file_path[:idx].to_owned()
None => "."
}
if spec.has_prefix("./") || spec.has_prefix("../") || spec == "." {
let base = dir + "/" + spec
for
cand in [
base,
base + ".d.ts",
base + ".ts",
base + ".d.cts",
base + ".d.mts",
base + "/index.d.ts",
base + "/index.ts",
] {
if @fs.exists(cand) {
return true
}
}
return false
}
// Bare specifier: the package directory is the first path segment (the
// first two for `@scope/name`). Walk up from the file's directory
// probing node_modules, mirroring Node resolution. Package presence is
// enough evidence to abstain -- subpath export validation is out of
// scope for a single-file check.
let head = if spec.has_prefix("@") {
match spec.find("/") {
Some(first) =>
match spec[first + 1:].find("/") {
Some(second) => spec[:first + 1 + second].to_owned()
None => spec
}
None => spec
}
} else {
match spec.find("/") {
Some(first) => spec[:first].to_owned()
None => spec
}
}
let mut cur = dir
for _ in 0..<64 {
if @fs.exists("\{cur}/node_modules/\{head}") ||
@fs.exists("\{cur}/node_modules/@types/\{head}") {
return true
}
match cur.rev_find("/") {
Some(idx) if idx > 0 => cur = cur[:idx].to_owned()
_ => break
}
}
false
}
///|
/// `start` is the index of the first option, so the verb dispatch in
/// `driver.mbt` passes `2`.
async fn mtsc_run_check(argv : Array[String], start : Int) -> Unit {
if start >= argv.length() {
println("Usage: mtsc check [--parse] [--iters N] [--strict] ")
return
}
let mut parse_only = false
let mut verbose = false
let mut strict = false
let mut iters = 1
let mut path = ""
let mut i = start
while i < argv.length() {
let arg = argv[i]
if arg == "--parse" {
parse_only = true
} else if arg == "--verbose" || arg == "-v" {
verbose = true
} else if arg == "--strict" {
strict = true
} else if arg == "--iters" && i + 1 < argv.length() {
i = i + 1
iters = try @string.parse_int(argv[i]) catch {
_ => 1
} noraise {
n if n > 0 => n
_ => 1
}
} else {
path = arg
}
i = i + 1
}
if path == "" {
println("Usage: mtsc check [--parse] [--iters N] [--strict] ")
return
}
let text = @fs.read_file(path).text() catch {
_ => {
println("read error: " + path)
return
}
}
let allow_jsx = path.has_suffix(".tsx")
let mut module_count = 0
let mut issue_count = 0
for _ in 0.. {
println("parse error: \{e}")
return
}
}
module_count = module_count + module_.funcs.length()
if !parse_only {
let issues = if strict {
@checker.check_module_function_bodies(module_)
} else {
@checker.check_module_function_bodies_permissive(module_)
}
// TS1046 applies only to declaration files -- keyed off the file
// extension, which the general check pipeline can't see.
if path.has_suffix(".d.ts") {
for iss in @checker.check_dts_top_level_modifiers(module_) {
issues.push(iss)
}
}
// Files inside node_modules are real packages whose imports resolve
// against the filesystem, which the single-file checker can't see.
// Drop TS2307 issues whose specifier actually resolves on disk.
let kept = if path.find("node_modules") is None {
issues
} else {
let out : Array[@checker.ExprIssue] = []
for iss in issues {
let prefix = "cannot find module `"
if iss.message.has_prefix(prefix) && iss.message.has_suffix("`") {
let spec = iss.message[prefix.length():iss.message.length() - 1].to_owned()
if module_spec_resolves_on_disk(path, spec) {
continue
}
}
out.push(iss)
}
out
}
issue_count = issue_count + kept.length()
if verbose {
for iss in kept {
println(" ISSUE [\{iss.path}]: \{iss.message}")
}
}
}
}
// Trivial output so the binary doesn't get fully optimized away;
// hyperfine pipes stdout to /dev/null anyway.
if parse_only {
println("parsed \{iters}x — \{module_count} funcs total")
} else {
println(
"checked \{iters}x — \{module_count} funcs, \{issue_count} issues total",
)
}
}