@@ -221,3 +221,156 @@ fn fail_fixtures_emit_exactly_their_expected_codes() {
221221 failures. join( "\n " )
222222 ) ;
223223}
224+
225+ /// The workspace root, from this crate's manifest directory.
226+ fn workspace_root ( ) -> PathBuf {
227+ Path :: new ( env ! ( "CARGO_MANIFEST_DIR" ) )
228+ . join ( "../.." )
229+ . canonicalize ( )
230+ . expect ( "workspace root should exist" )
231+ }
232+
233+ /// Every `.rssi` under `directory`, recursively, sorted so failures report
234+ /// stably.
235+ fn interface_files ( directory : & Path , found : & mut Vec < PathBuf > ) {
236+ let mut entries = fs:: read_dir ( directory)
237+ . unwrap_or_else ( |error| panic ! ( "read {}: {error}" , directory. display( ) ) )
238+ . map ( |entry| entry. expect ( "directory entry" ) . path ( ) )
239+ . collect :: < Vec < _ > > ( ) ;
240+ entries. sort ( ) ;
241+ for path in entries {
242+ if path. is_dir ( ) {
243+ interface_files ( & path, found) ;
244+ } else if path
245+ . extension ( )
246+ . is_some_and ( |extension| extension == "rssi" )
247+ {
248+ found. push ( path) ;
249+ }
250+ }
251+ }
252+
253+ /// A signature-level rule holds wherever the signature is written. An `.rssi`
254+ /// declaration has no body, but it has a contract, and an interface that
255+ /// escaped the rule could export one the language does not have — `pub fn
256+ /// make_default<T>() -> fresh T` was accepted in an interface while the same
257+ /// signature in a `.rss` file was `RS0603`.
258+ ///
259+ /// The diagnostic must also land on the interface, not on the source that
260+ /// supplied it: the reader has to be sent to the file they can fix.
261+ #[ test]
262+ fn invalid_interface_signatures_are_diagnosed_against_the_interface_file ( ) {
263+ const SOURCE : & str = "fn main() -> Unit {\n return Unit\n }\n " ;
264+ const INTERFACE : & str = "pub fn make_default<T>() -> fresh T\n " ;
265+
266+ let mut interfaces = standard_package_interfaces ( ) . to_vec ( ) ;
267+ interfaces. push ( ( "host/defaults.rssi" , INTERFACE ) ) ;
268+ let diagnostics = analyze_sources_with_interfaces ( & [ ( "main.rss" , SOURCE ) ] , & interfaces) ;
269+
270+ let invalid_fresh = diagnostics
271+ . iter ( )
272+ . find ( |diagnostic| diagnostic. code == "RS0603" )
273+ . unwrap_or_else ( || {
274+ panic ! (
275+ "an invalid `.rssi` signature must be diagnosed; got {:?}" ,
276+ diagnostics
277+ . iter( )
278+ . map( |diagnostic| diagnostic. code. as_str( ) )
279+ . collect:: <Vec <_>>( )
280+ )
281+ } ) ;
282+ assert_eq ! (
283+ invalid_fresh. span. file, "host/defaults.rssi" ,
284+ "the diagnostic must point at the interface that declares the signature"
285+ ) ;
286+ assert ! (
287+ invalid_fresh. summary. contains( "make_default" ) ,
288+ "the diagnostic must name the interface declaration: {}" ,
289+ invalid_fresh. summary
290+ ) ;
291+
292+ // The bounded form of the same signature is clean, so the rule is not
293+ // rejecting every generic `fresh` return in an interface.
294+ let mut bounded = standard_package_interfaces ( ) . to_vec ( ) ;
295+ bounded. push ( (
296+ "host/defaults.rssi" ,
297+ "pub fn make_default<T: Struct>() -> fresh T\n " ,
298+ ) ) ;
299+ assert ! (
300+ analyze_sources_with_interfaces( & [ ( "main.rss" , SOURCE ) ] , & bounded) . is_empty( ) ,
301+ "a correctly bounded interface signature must stay clean"
302+ ) ;
303+ }
304+
305+ /// The prelude is the one interface set every program sees, so a regression in
306+ /// it would be invisible in ordinary fixtures until it reached users. Check
307+ /// every `.rssi` that ships, read from disk rather than from the embedded
308+ /// catalog, so an interface added to `stdlib/` or `packages/` is covered the
309+ /// day it lands.
310+ #[ test]
311+ fn every_shipped_interface_passes_the_signature_checks ( ) {
312+ const SOURCE : & str = "fn main() -> Unit {\n return Unit\n }\n " ;
313+
314+ let root = workspace_root ( ) ;
315+ let mut paths = Vec :: new ( ) ;
316+ interface_files ( & root. join ( "stdlib" ) , & mut paths) ;
317+ for package in {
318+ let mut packages = fs:: read_dir ( root. join ( "packages" ) )
319+ . expect ( "packages directory should exist" )
320+ . map ( |entry| entry. expect ( "packages entry" ) . path ( ) )
321+ . collect :: < Vec < _ > > ( ) ;
322+ packages. sort ( ) ;
323+ packages
324+ } {
325+ let interface = package. join ( "interface" ) ;
326+ if interface. is_dir ( ) {
327+ interface_files ( & interface, & mut paths) ;
328+ }
329+ }
330+ assert ! (
331+ paths. len( ) >= 30 ,
332+ "the shipped interface set should not have shrunk to {} files" ,
333+ paths. len( )
334+ ) ;
335+
336+ // Supply the whole set at once: the interfaces reference each other's
337+ // protocols (`Ord`, `Eq`, `Hashable`), so checking one in isolation would
338+ // report a missing protocol that the prelude does in fact declare. Each
339+ // diagnostic still carries its own interface's path, so a failure names
340+ // the file to fix.
341+ let sources = paths
342+ . iter ( )
343+ . map ( |path| {
344+ let relative = path
345+ . strip_prefix ( & root)
346+ . unwrap_or ( path)
347+ . to_string_lossy ( )
348+ . into_owned ( ) ;
349+ let text = fs:: read_to_string ( path)
350+ . unwrap_or_else ( |error| panic ! ( "read {}: {error}" , path. display( ) ) ) ;
351+ ( relative, text)
352+ } )
353+ . collect :: < Vec < _ > > ( ) ;
354+ let interfaces = sources
355+ . iter ( )
356+ . map ( |( file, text) | ( file. as_str ( ) , text. as_str ( ) ) )
357+ . collect :: < Vec < _ > > ( ) ;
358+
359+ let failures = analyze_sources_with_interfaces ( & [ ( "main.rss" , SOURCE ) ] , & interfaces)
360+ . into_iter ( )
361+ . map ( |diagnostic| {
362+ format ! (
363+ "{}:{}:{}: {}" ,
364+ diagnostic. span. file, diagnostic. span. line, diagnostic. span. column, diagnostic. code
365+ )
366+ } )
367+ . collect :: < Vec < _ > > ( ) ;
368+
369+ assert ! (
370+ failures. is_empty( ) ,
371+ "{} diagnostics across the {} shipped interfaces:\n {}" ,
372+ failures. len( ) ,
373+ paths. len( ) ,
374+ failures. join( "\n " )
375+ ) ;
376+ }
0 commit comments