char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somext_build: support generating typedefs from typescript implf90c0ce

main
13.1 KiB354 linesraw
1use super::*;
2
3#[test]
4fn erases_types_without_removing_value_imports_or_leaking_private_modules() {
5    let module = "avril:quoted\"name";
6    let compiled = compile(
7        Path::new("src/api.ts"),
8        r#"
9        import type { Internal } from "ext:private/types.js";
10        import { unused } from "ext:private/value.js";
11        const internal: Internal | null = null;
12        export function identity(value: unknown): unknown { return value; }
13        "#,
14        Some(module),
15    )
16    .unwrap();
17    let allocator = Allocator::default();
18    let javascript = Parser::new(&allocator, &compiled.javascript, SourceType::mjs()).parse();
19    assert!(
20        javascript.diagnostics.is_empty(),
21        "{:?}",
22        javascript.diagnostics
23    );
24    let imports: Vec<_> = javascript
25        .program
26        .body
27        .iter()
28        .filter_map(|statement| match statement {
29            Statement::ImportDeclaration(import) => Some(import.source.value.as_str()),
30            _ => None,
31        })
32        .collect();
33    assert_eq!(imports, ["ext:private/value.js"]);
34
35    let declaration = compiled.declaration.unwrap();
36    let declarations = Parser::new(&allocator, &declaration, SourceType::d_ts()).parse();
37    assert!(
38        declarations.diagnostics.is_empty(),
39        "{:?}",
40        declarations.diagnostics
41    );
42    assert_eq!(declarations.program.body.len(), 1);
43    let Statement::TSExternalModuleDeclaration(public) = &declarations.program.body[0] else {
44        panic!("Expected an ambient public module");
45    };
46    assert!(public.declare);
47    assert_eq!(public.id.value.as_str(), module);
48    let body = &public.body.as_ref().unwrap().body;
49    assert_eq!(body.len(), 1);
50    let Statement::ExportDeclaration(export) = &body[0] else {
51        panic!("Expected the public function");
52    };
53    let Declaration::FunctionDeclaration(function) = &export.declaration else {
54        panic!("Expected a function declaration");
55    };
56    assert_eq!(function.id.as_ref().unwrap().name.as_str(), "identity");
57    assert!(function.body.is_none());
58    assert!(!function.declare);
59}
60
61#[test]
62fn internal_modules_do_not_require_isolated_declarations_or_function_only_exports() {
63    let compiled = compile(
64        Path::new("src/internal.ts"),
65        "import 'ext:private/setup.js'; export class State { #value: number = 42; value() { return this.#value; } }",
66        None,
67    )
68    .unwrap();
69    assert!(compiled.declaration.is_none());
70    let allocator = Allocator::default();
71    let parsed = Parser::new(&allocator, &compiled.javascript, SourceType::mjs()).parse();
72    assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics);
73    let Statement::ImportDeclaration(import) = &parsed.program.body[0] else {
74        panic!("Expected the side-effect import");
75    };
76    assert_eq!(import.source.value.as_str(), "ext:private/setup.js");
77    let Statement::ExportDeclaration(export) = &parsed.program.body[1] else {
78        panic!("Expected the native class export");
79    };
80    assert!(matches!(
81        export.declaration,
82        Declaration::ClassDeclaration(_)
83    ));
84}
85
86#[test]
87fn isolated_declaration_diagnostics_are_propagated() {
88    assert!(
89        compile(
90            Path::new("src/api.ts"),
91            "export function value() { return 42; }",
92            Some("avril:fixture"),
93        )
94        .is_ok()
95    );
96    let error = compile(
97        Path::new("src/api.ts"),
98        "export function value() { return Math.random(); }",
99        Some("avril:fixture"),
100    )
101    .err()
102    .expect("Nontrivial return expressions require an annotation");
103    assert!(error.to_string().contains("return type"), "{error}");
104}
105
106#[test]
107fn unsupported_public_declarations_fail_instead_of_being_dropped() {
108    for source in [
109        "export enum Value { One }",
110        "export namespace Value { export type Inner = number; }",
111        "export class Value {}",
112        "export { value } from 'ext:private/main.js';",
113        "export default function value(): number { return 42; }",
114        "import type { Value } from 'ext:private/types.js'; export function value(v: Value): Value { return v; }",
115    ] {
116        let error = compile(Path::new("src/api.ts"), source, Some("avril:fixture"))
117            .err()
118            .expect("Unsupported public declarations must fail");
119        assert!(
120            error.to_string().contains("Unsupported public declaration"),
121            "{source}: {error}"
122        );
123    }
124}
125
126#[test]
127fn packages_callable_intersections_and_their_exported_types() {
128    let compiled = compile(
129        Path::new("src/public.ts"),
130        r#"
131        import { registration } from "ext:private/main.js";
132        export interface Options { name: string; }
133        export type Register = ((name: string) => void) & ((options: Options) => void);
134        export const register: Register & { only: Register } = registration;
135        "#,
136        Some("avril:fixture"),
137    )
138    .unwrap();
139    let declaration = compiled.declaration.unwrap();
140    assert!(!declaration.contains("ext:private"));
141    let allocator = Allocator::default();
142    let parsed = Parser::new(&allocator, &declaration, SourceType::d_ts()).parse();
143    assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics);
144    let Statement::TSExternalModuleDeclaration(public) = &parsed.program.body[0] else {
145        panic!("Expected an ambient public module");
146    };
147    let exports: Vec<_> = public
148        .body
149        .as_ref()
150        .unwrap()
151        .body
152        .iter()
153        .map(|statement| {
154            let Statement::ExportDeclaration(export) = statement else {
155                panic!("Expected a public export");
156            };
157            match &export.declaration {
158                Declaration::TSInterfaceDeclaration(interface) => interface.id.name.as_str(),
159                Declaration::TSTypeAliasDeclaration(alias) => alias.id.name.as_str(),
160                Declaration::VariableDeclaration(variable) => {
161                    assert!(!variable.declare);
162                    assert_eq!(variable.declarations.len(), 1);
163                    assert!(variable.declarations[0].init.is_none());
164                    let oxc::ast::ast::BindingPattern::BindingIdentifier(id) =
165                        &variable.declarations[0].id
166                    else {
167                        panic!("Expected the exported binding");
168                    };
169                    id.name.as_str()
170                }
171                _ => panic!("Unexpected public declaration"),
172            }
173        })
174        .collect();
175    assert_eq!(exports, ["Options", "Register", "register"]);
176}
177
178#[test]
179fn private_types_are_kept_without_being_exported() {
180    let compiled = compile(
181        Path::new("src/public.ts"),
182        r#"
183        type Hidden = { value: number };
184        interface AlsoHidden { text: string; }
185        import { convert as implementation } from "./internal.js";
186        export const convert: (input: Hidden) => AlsoHidden = implementation;
187        "#,
188        Some("avril:fixture"),
189    )
190    .unwrap();
191    let declaration = compiled.declaration.unwrap();
192    assert!(!declaration.contains("./internal.js"));
193    let allocator = Allocator::default();
194    let parsed = Parser::new(&allocator, &declaration, SourceType::d_ts()).parse();
195    assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics);
196    let Statement::TSExternalModuleDeclaration(public) = &parsed.program.body[0] else {
197        panic!("Expected an ambient public module");
198    };
199    let mut exported = 0;
200    let mut markers = 0;
201    let mut private = Vec::new();
202    for statement in public.body.as_ref().unwrap().body.iter() {
203        match statement {
204            Statement::ExportDeclaration(_) => exported += 1,
205            Statement::ExportNamedDeclaration(export) => {
206                assert!(export.specifiers.is_empty());
207                markers += 1;
208            }
209            Statement::TSInterfaceDeclaration(interface) => {
210                private.push(interface.id.name.as_str());
211            }
212            Statement::TSTypeAliasDeclaration(alias) => private.push(alias.id.name.as_str()),
213            statement => panic!("Unexpected public statement: {statement:?}"),
214        }
215    }
216    assert_eq!(exported, 1);
217    // The marker is required even though the body has exports: without it
218    // the private declarations would be importable.
219    assert_eq!(markers, 1);
220    assert_eq!(private, ["Hidden", "AlsoHidden"]);
221}
222
223#[test]
224fn public_type_imports_and_reexports_are_preserved_verbatim() {
225    let compiled = compile(
226        Path::new("src/public/sync.ts"),
227        r#"
228        import type { CopyOptions } from "avril:fs";
229        export type { CopyOptions } from "avril:fs";
230        import { copy as implementation } from "../fs_sync.js";
231        export const copy: (source: string, options?: CopyOptions) => void = implementation;
232        "#,
233        Some("avril:fixture/sync"),
234    )
235    .unwrap();
236    let declaration = compiled.declaration.unwrap();
237    assert!(declaration.contains("from \"avril:fs\""));
238    assert!(!declaration.contains("../fs_sync.js"));
239    let allocator = Allocator::default();
240    let parsed = Parser::new(&allocator, &declaration, SourceType::d_ts()).parse();
241    assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics);
242    let Statement::TSExternalModuleDeclaration(public) = &parsed.program.body[0] else {
243        panic!("Expected an ambient public module");
244    };
245    let mut imports = 0;
246    let mut reexports = 0;
247    let mut exports = 0;
248    for statement in public.body.as_ref().unwrap().body.iter() {
249        match statement {
250            Statement::ImportDeclaration(import) => {
251                assert_eq!(import.source.value.as_str(), "avril:fs");
252                imports += 1;
253            }
254            Statement::ExportFromDeclaration(export) => {
255                assert_eq!(export.source.value.as_str(), "avril:fs");
256                reexports += 1;
257            }
258            Statement::ExportDeclaration(_) => exports += 1,
259            statement => panic!("Unexpected public statement: {statement:?}"),
260        }
261    }
262    assert_eq!((imports, reexports, exports), (1, 1, 1));
263}
264
265#[test]
266fn private_module_references_fail_instead_of_being_bundled() {
267    for source in [
268        "import type { Value } from './types.js'; export function value(v: Value): Value { return v; }",
269        "export type { Value } from './types.js';",
270        "import { Value } from 'avril:fixture'; import { value as implementation } from './internal.js'; export const value: (v: Value) => Value = implementation;",
271        "export { Value } from 'avril:fixture';",
272    ] {
273        let error = compile(Path::new("src/api.ts"), source, Some("avril:fixture"))
274            .err()
275            .expect("Private module references must fail");
276        assert!(
277            error.to_string().contains("Unsupported public declaration"),
278            "{source}: {error}"
279        );
280    }
281}
282
283#[test]
284fn outputs_mirror_source_paths_under_src() {
285    use std::path::PathBuf;
286    let out = Path::new("target");
287    assert_eq!(
288        output_path(out, Path::new("src/main.ts")).unwrap(),
289        PathBuf::from("target/main.ts")
290    );
291    assert_eq!(
292        output_path(out, Path::new("src/public.ts")).unwrap(),
293        PathBuf::from("target/public.ts")
294    );
295    assert_eq!(
296        output_path(out, Path::new("src/public/posix.ts")).unwrap(),
297        PathBuf::from("target/public/posix.ts")
298    );
299    output_path(out, Path::new("other.ts")).expect_err("Sources outside src/ must fail");
300}
301
302#[test]
303fn invalid_syntax_is_rejected_with_source_diagnostics() {
304    let error = compile(Path::new("src/broken.ts"), "export function broken(", None)
305        .err()
306        .expect("Invalid syntax must fail");
307    assert!(
308        error.to_string().contains("export function broken("),
309        "{error}"
310    );
311}
312
313#[test]
314fn implementation_only_declarations_are_pruned_from_the_public_declaration() {
315    let compiled = compile(
316        Path::new("src/public.ts"),
317        r#"
318        import { factory } from "ext:core/ops";
319        import type { Unused } from "./internal.js";
320        interface State { entries: Map<string, string>; }
321        const state = new Map<string, State>();
322        class Impl { run(): void { factory(); } }
323        export interface Surface { readonly value: number; }
324        export function read(): Surface { return null as unknown as Surface; }
325        "#,
326        Some("avril:fixture"),
327    )
328    .unwrap();
329    let declaration = compiled.declaration.unwrap();
330    assert!(!declaration.contains("ext:core/ops"), "{declaration}");
331    assert!(!declaration.contains("./internal.js"), "{declaration}");
332    assert!(!declaration.contains("State"), "{declaration}");
333    assert!(!declaration.contains("Impl"), "{declaration}");
334    assert!(
335        declaration.contains("export interface Surface"),
336        "{declaration}"
337    );
338}
339
340#[test]
341fn side_effect_imports_do_not_reach_the_public_declaration() {
342    let compiled = compile(
343        Path::new("src/public.ts"),
344        "import 'ext:private/setup.js'; export function value(): number { return 42; }",
345        Some("avril:fixture"),
346    )
347    .unwrap();
348    let declaration = compiled.declaration.unwrap();
349    assert!(!declaration.contains("ext:private"), "{declaration}");
350    assert!(
351        declaration.contains("export function value"),
352        "{declaration}"
353    );
354}