@@ -1341,7 +1341,6 @@ mod test {
13411341 }
13421342
13431343 /// Path to the committed TypeScript bindings artifact.
1344- #[ cfg( feature = "ts-rs" ) ]
13451344 fn dts_path ( ) -> PathBuf {
13461345 Path :: new ( env ! ( "CARGO_MANIFEST_DIR" ) ) . join ( "bindings.d.ts" )
13471346 }
@@ -1392,6 +1391,89 @@ mod test {
13921391 std:: fs:: write ( dts_path ( ) , generate_cli_bindings_dts ( ) ) . unwrap ( ) ;
13931392 }
13941393
1394+ /// `bindings.d.ts` is vendored into other repos as a standalone file
1395+ /// (sp331's `services/tmc/src/tmc/cli.d.ts`), so a type it references but
1396+ /// never declares makes it invalid TypeScript there. The byte-equality
1397+ /// gates compare bytes only and cannot see that; a type reachable from an
1398+ /// exported type but missing from `generate_cli_bindings_dts`'s
1399+ /// `export_to!` list is the way it happens.
1400+ #[ test]
1401+ fn bindings_dts_declares_every_type_it_references ( ) {
1402+ /// Types TypeScript provides; everything else must be declared in-file.
1403+ const TS_BUILTINS : [ & str ; 2 ] = [ "Array" , "Record" ] ;
1404+
1405+ let src = strip_comments_and_strings (
1406+ & std:: fs:: read_to_string ( dts_path ( ) ) . expect ( "bindings.d.ts should exist" ) ,
1407+ ) ;
1408+
1409+ let mut declared = std:: collections:: HashSet :: new ( ) ;
1410+ for line in src. lines ( ) {
1411+ let Some ( rest) = line. strip_prefix ( "export type " ) else {
1412+ continue ;
1413+ } ;
1414+ let name_end = rest
1415+ . find ( |c : char | !c. is_alphanumeric ( ) && c != '_' )
1416+ . unwrap_or ( rest. len ( ) ) ;
1417+ declared. insert ( & rest[ ..name_end] ) ;
1418+ // A generic alias declares its own parameters.
1419+ if let Some ( params) = rest[ name_end..]
1420+ . strip_prefix ( '<' )
1421+ . and_then ( |r| r. split_once ( '>' ) )
1422+ . map ( |( params, _) | params)
1423+ {
1424+ declared. extend ( params. split ( ',' ) . map ( str:: trim) ) ;
1425+ }
1426+ }
1427+
1428+ let mut missing = src
1429+ . split ( |c : char | !c. is_alphanumeric ( ) && c != '_' )
1430+ . filter ( |word| word. starts_with ( |c : char | c. is_ascii_uppercase ( ) ) )
1431+ . filter ( |word| !TS_BUILTINS . contains ( word) && !declared. contains ( word) )
1432+ . collect :: < Vec < _ > > ( ) ;
1433+ missing. sort_unstable ( ) ;
1434+ missing. dedup ( ) ;
1435+
1436+ assert ! (
1437+ missing. is_empty( ) ,
1438+ "bindings.d.ts references types it does not declare: {missing:?}; \
1439+ add them to generate_cli_bindings_dts's export_to! list and regenerate"
1440+ ) ;
1441+ }
1442+
1443+ /// Blanks out doc comments and string literals so type references can be
1444+ /// picked out by identifier casing without prose or literal unions
1445+ /// (`"tar" | "zip"`) being mistaken for them.
1446+ fn strip_comments_and_strings ( src : & str ) -> String {
1447+ let mut out = String :: with_capacity ( src. len ( ) ) ;
1448+ let mut chars = src. chars ( ) . peekable ( ) ;
1449+ while let Some ( c) = chars. next ( ) {
1450+ match c {
1451+ '/' if chars. peek ( ) == Some ( & '*' ) => {
1452+ let mut prev = ' ' ;
1453+ for c in chars. by_ref ( ) {
1454+ if prev == '*' && c == '/' {
1455+ break ;
1456+ }
1457+ prev = c;
1458+ }
1459+ }
1460+ '"' => {
1461+ while let Some ( c) = chars. next ( ) {
1462+ match c {
1463+ '\\' => {
1464+ chars. next ( ) ;
1465+ }
1466+ '"' => break ,
1467+ _ => { }
1468+ }
1469+ }
1470+ }
1471+ _ => out. push ( c) ,
1472+ }
1473+ }
1474+ out
1475+ }
1476+
13951477 /// Produces the TypeScript bindings as a string from the current types.
13961478 #[ cfg( feature = "ts-rs" ) ]
13971479 fn generate_cli_bindings_dts ( ) -> String {
@@ -1492,11 +1574,15 @@ mod test {
14921574 tmc_langs:: mooc:: TmcExerciseSlide ,
14931575 tmc_langs:: mooc:: TmcExerciseTask ,
14941576 tmc_langs:: mooc:: PublicSpec ,
1577+ tmc_langs:: mooc:: ExerciseType ,
1578+ tmc_langs:: mooc:: BrowserTestSpec ,
1579+ tmc_langs:: mooc:: BrowserTestRuntime ,
14951580 tmc_langs:: mooc:: ModelSolutionSpec ,
14961581 tmc_langs:: mooc:: ExerciseTaskSubmissionResult ,
14971582 tmc_langs:: mooc:: ExerciseTaskSubmissionStatus ,
14981583 tmc_langs:: mooc:: GradingProgress ,
14991584 tmc_langs:: mooc:: ExerciseSlideSubmissionListItem ,
1585+ tmc_langs:: mooc:: PasteResult ,
15001586 tmc_langs:: mooc:: CourseProgress ,
15011587 tmc_langs:: mooc:: ExerciseProgress ,
15021588 tmc_langs:: mooc:: MoocClientUpdateData ,
0 commit comments