Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/compiler/aot.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ The build only stops where `tsc` reports an error under your own `tsconfig.json`
- If `@types/bun` is installed but `types` doesn't name it, the build still reads it, and an error that only comes from its declarations doesn't stop the build.
- Without a `tsconfig.json`, the build uses [the options of `bun check`](/runtime/check#without-a-tsconfigjson) with TypeScript's default `lib`, which declares the DOM. `typeof window` and `HTMLElement` are not errors.

A few options can't change from one file to the next, because a build has one type checker and the entry point's `tsconfig.json` sets it up: `strictNullChecks`, `strictFunctionTypes`, `strictBindCallApply`, `strictBuiltinIteratorReturn`, `exactOptionalPropertyTypes`, `lib` (or the `target` it defaults from), `allowJs`, `maxNodeModuleJsDepth`, `resolveJsonModule` and `allowArbitraryExtensions`. If a folder's `tsconfig.json` sets one of them differently, the build doesn't type check the files in that folder, and its last line counts them as compiled without types. It still reads their types, but takes each one as a [guess](/compiler/internals#what-types-are-used-for) that the compiled code checks. They behave the same. Run `bun check` in that folder to see their type errors. A package that [publishes its types](/compiler/libraries#publish-a-package-with-its-types) is the exception: it keeps them whatever your `tsconfig.json` says.
A few options can't change from one file to the next, because a build has one type checker and the entry point's `tsconfig.json` sets it up: `strictNullChecks`, `strictFunctionTypes`, `strictBindCallApply`, `strictBuiltinIteratorReturn`, `exactOptionalPropertyTypes`, `lib` (or the `target` it defaults from), `allowJs`, `maxNodeModuleJsDepth`, `resolveJsonModule` and `allowArbitraryExtensions`. If a folder's `tsconfig.json` sets one of them differently, the build doesn't type check the files in that folder, and its last line counts the TypeScript files it compiles there as compiled without types. It still reads their types, but takes each one as a [guess](/compiler/internals#what-types-are-used-for) that the compiled code checks. They behave the same. Run `bun check` in that folder to see their type errors. A package that [publishes its types](/compiler/libraries#publish-a-package-with-its-types) is the exception: it keeps them whatever your `tsconfig.json` says.

During development, [`bun run --aot`](/compiler/debugging#run-under-the-compilers-rules) runs the same checks and then runs your program from its source. [`bun test --aot`](/compiler/debugging#run-your-tests-compiled) compiles your tests and runs the compiled code.

Expand Down
9 changes: 5 additions & 4 deletions src/aot/report/file_counts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,11 @@ pub(crate) fn count_files(
let demoted_files = roles.demoted.iter().map(|demotion| demotion.file);
let demoted_count = demoted_files
.filter(|&file| {
!matches!(
view.origin(file),
FileOrigin::Runtime | FileOrigin::EvaluatedText
)
files.is_compiled(file)
&& !matches!(
view.origin(file),
FileOrigin::Runtime | FileOrigin::EvaluatedText
)
})
.count();
statistics.count(StatisticId::SourceFilesDemoted, demoted_count as u64);
Expand Down
37 changes: 36 additions & 1 deletion src/aot/report/file_counts/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ fn role_decision(view: ProgramView<'_, '_>, demoted: &[FileId]) -> RoleDecision
let files = view.files();
let file_ids = (0..files.modules.len()).map(|index| FileId(index as u32));
let cause = DemotionCause::UnverifiedTypes;
let mut roles: Vec<FileRole> = file_ids.map(|file| files.role(file)).collect();
for &file in demoted {
roles[file.idx()] = FileRole::Inferred;
}
RoleDecision {
roles: file_ids.map(|file| files.role(file)).collect(),
roles,
demoted: demoted
.iter()
.map(|&file| Demotion { file, cause })
Expand Down Expand Up @@ -49,6 +53,15 @@ fn file_at(view: ProgramView<'_, '_>, suffix: &str) -> FileId {
.unwrap_or_else(|| panic!("{suffix} is not compiled"))
}

fn file_that_is_not_compiled(view: ProgramView<'_, '_>, suffix: &str) -> FileId {
let files = view.files();
let mut file_ids = (0..files.modules.len()).map(|index| FileId(index as u32));
let found = file_ids.find(|&file| files.module(file).file_name().ends_with(suffix.as_bytes()));
let file = found.unwrap_or_else(|| panic!("{suffix} is not in the program"));
assert!(!files.is_compiled(file), "{suffix} is compiled");
file
}

#[test]
fn types_only_role_decides_before_origin() {
for origin in FileOrigin::ALL {
Expand Down Expand Up @@ -221,6 +234,28 @@ fn demoted_files_are_counted() {
});
}

#[test]
fn demoted_file_that_is_not_compiled_is_not_counted() {
let files = [
(
"a.ts",
"import { b } from \"./b\";\nimport type { Kind } from \"./kind\";\nexport const a: Kind = b;\n",
),
("b.ts", "export const b = 1;\n"),
("kind.ts", "export type Kind = number;\n"),
];
with_project_view(&files, |view| {
let demoted = [
file_at(view, "/b.ts"),
file_that_is_not_compiled(view, "/kind.ts"),
];
let statistics = count(view, &role_decision(view, &demoted));
assert_eq!(statistics.get(StatisticId::SourceFilesChecked), 1);
assert_eq!(statistics.get(StatisticId::SourceFilesInferred), 1);
assert_eq!(statistics.get(StatisticId::SourceFilesDemoted), 1);
});
}

#[test]
fn runtime_project_adds_neither_text_nor_demotions() {
let sorts = [("a.ts", "console.log([2, 1].sort());\n")];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// RUN: lower
// OPTIONS: builtin-modules
// STATISTIC: SourceFilesChecked = 1
// STATISTIC: SourceFilesInferred = 2
// STATISTIC: SourceFilesDemoted = 2

// FILE: main.ts
// strict/ has a tsconfig.json whose compiler options are not the program's. A constant of the entry point, true because the program starts with it, decides there that an `import()` never runs, so the executable does not hold the file that it names.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// RUN: lower
// STATISTIC: SourceFilesChecked = 1
// STATISTIC: SourceFilesInferred = 1
// STATISTIC: SourceFilesData = 1
// STATISTIC: SourceFilesDemoted = 1

// The tsconfig.json of the entry point names inner/text.ts as a file of the project, and the program embeds it as text.
// Neither makes it a compiled file: of the two files under a tsconfig.json that needs another program, only the one that is
// imported for its value is compiled without its types.

// FILE: main.ts
import { value } from "./inner/lib.ts";
import text from "./inner/text.ts" with { type: "text" };
(globalThis as any).console.log(value, text.length);

// FILE: tsconfig.json
{"compilerOptions":{"target":"esnext","module":"preserve","moduleResolution":"bundler","noEmit":true,"allowImportingTsExtensions":true,"strict":true,"skipLibCheck":true,"lib":["esnext","dom"]}}

// FILE: inner/tsconfig.json
{"compilerOptions":{"target":"esnext","module":"preserve","moduleResolution":"bundler","noEmit":true,"allowImportingTsExtensions":true,"strict":true,"skipLibCheck":true,"lib":["esnext"]}}

// FILE: inner/lib.ts
export const value = typeof document;

// FILE: inner/text.ts
export const embedded = typeof document;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// RUN: lower
// STATISTIC: SourceFilesChecked = 1
// STATISTIC: SourceFilesInferred = 1
// STATISTIC: SourceFilesDemoted = 1

// CHECK: function {{*}}name="area" {{*}}parameters=[{type=undefined|null|[[size]] flags=[IsExpected]}] results=[{type=number}]

// Of the files under a tsconfig.json that needs another program, only the one that is compiled is compiled without its types.
// A file that is imported for a type, and a file that nothing imports, are read for what they declare. They have unverified
// types all the same: what they declare is only expected, as the parameter of `area` shows.

// FILE: main.ts
import { value } from "./inner/lib.ts";
import type { Size } from "./inner/sizes.ts";
export function area(size: Size): number {
return size.width * size.height;
}
(globalThis as any).console.log(value, area({ width: 2, height: 3 }));

// FILE: tsconfig.json
{"compilerOptions":{"target":"esnext","module":"preserve","moduleResolution":"bundler","noEmit":true,"allowImportingTsExtensions":true,"strict":true,"skipLibCheck":true,"lib":["esnext","dom"]}}

// FILE: inner/tsconfig.json
{"compilerOptions":{"target":"esnext","module":"preserve","moduleResolution":"bundler","noEmit":true,"allowImportingTsExtensions":true,"strict":true,"skipLibCheck":true,"lib":["esnext"]}}

// FILE: inner/lib.ts
export const value = typeof document;

// FILE: inner/sizes.ts
export interface Size {
width: number;
height: number;
}

// FILE: inner/nothing-imports-this.ts
export const unused = 1;
114 changes: 114 additions & 0 deletions test/aot/summary-line.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,117 @@ test(
}),
buildTimeout,
);

// The closing line also counts the files that are compiled without their types: a file that begins with `// @ts-nocheck`, and a
// file whose tsconfig.json needs another program than the entry point's (docs/compiler/aot.mdx lists the options that do). A
// tsconfig.json without "include" names every file below it, and a build reads each of them for what it declares, as it reads a
// file that is imported for a type. Neither is compiled, and neither is a file that is embedded as text, so none of them is counted.
const sameInBoth = {
strict: true,
module: "preserve",
moduleResolution: "bundler",
target: "esnext",
noEmit: true,
allowImportingTsExtensions: true,
types: ["bun"],
lib: ["esnext"],
};
const tsconfig = (more: object = {}) => JSON.stringify({ compilerOptions: { ...sameInBoth, ...more } });
const threeModules: Files = {
"web/a.ts": "export const a = 1;\n",
"web/b.ts": "export const b = 2;\n",
"web/c.ts": "export const c = 3;\n",
};
const folderWithAnotherLib: Files = {
"tsconfig.json": tsconfig(),
"web/tsconfig.json": tsconfig({ lib: ["esnext", "dom"] }),
...threeModules,
};
// The tsconfig.json that `bun init` writes allows JavaScript, so a folder whose own one does not say so too needs another program.
const folderBelowOneThatAllowsJavaScript: Files = {
"tsconfig.json": tsconfig({ allowJs: true }),
"web/tsconfig.json": "{}",
...threeModules,
};
const filesWithoutChecks: Files = {
"tsconfig.json": tsconfig(),
"a.ts": `// @ts-nocheck\nexport const a: number = "1";\n`,
"b.ts": `// @ts-nocheck\nexport const b: number = "2";\n`,
"c.ts": `// @ts-nocheck\nexport {};\ndeclare global {\n var c: number;\n}\n`,
};
const oneFileAndTheTypeOfAnother = (folder: string) =>
`import { a } from "${folder}/a";\nimport type { b } from "${folder}/b";\nconst two: typeof b = 2;\n`;
const oneFileAndTheTextOfAnother = `import { a } from "./web/a";\nimport text from "./web/b.ts" with { type: "text" };\n`;
const oneOfThreeIsCompiled: Files = {
...folderWithAnotherLib,
"main.ts": `${oneFileAndTheTypeOfAnother("./web")}console.log(a, two);\n`,
};
const untypedFiles: [string, Files, string][] = [
[
"nothing of a folder whose tsconfig.json is empty, below one that allows JavaScript",
{ ...folderBelowOneThatAllowsJavaScript, "main.ts": "console.log(1);\n" },
"1 file",
],
[
"one file of a folder with another lib, and the type of another",
oneOfThreeIsCompiled,
"2 files, 1 file compiled without types",
],
[
"one file of such a folder, and the text of another",
{ ...folderWithAnotherLib, "main.ts": `${oneFileAndTheTextOfAnother}console.log(a, text.length);\n` },
"3 files, 1 file compiled without types",
],
[
"one file that begins with // @ts-nocheck, the type of another, and a global that a third declares",
{
...filesWithoutChecks,
"main.ts": `${oneFileAndTheTypeOfAnother(".")}globalThis.c = 3;\nconsole.log(a, two, c);\n`,
},
"2 files, 1 file compiled without types",
],
];

describe.concurrent("closing line counts the files that are compiled without their types", () => {
test.each(untypedFiles)(
"the program imports %s",
(_, files, counts) =>
slot(async () => {
const closingLine = new RegExp(
` No type errors in ${counts}, compiled [\\d,]+ lines? in [\\d,]+\\.\\d\\d s \\([\\d,]+ lines/s\\)$`,
);
const { lines } = await check(files);
expect(lines.filter(line => line.includes("No type errors"))).toEqual([expect.stringMatching(closingLine)]);
}),
buildTimeout,
);

test(
"a build counts them alike, on its compile line and under --explain=statistics",
() =>
slot(async () => {
using dir = tempDirWithBunTypes("aot-summary-line", oneOfThreeIsCompiled);
const command = [
bunExe(),
"build",
"--compile",
"--aot",
"--explain=statistics",
"main.ts",
"--outfile",
"compiled",
];
const { stdout, stderr, exitCode, signal } = await spawned(command, String(dir), { ...bunEnv, NO_COLOR: "1" });
const said = exitCode === 0 ? "" : stdout + stderr;
expect({ exitCode, signal, said }).toEqual({ exitCode: 0, signal: null, said: "" });
const compileLine = / compile .* 1 file compiled without types, compiled [\d,]+ lines? in /;
expect(stdout.split("\n").filter(line => line.includes(" compile "))).toEqual([
expect.stringMatching(compileLine),
]);
expect(stderr.split("\n").filter(line => line.startsWith("SourceFilesDemoted "))).toEqual([
"SourceFilesDemoted 1",
]);
}),
buildTimeout,
);
});
Loading