@@ -13,7 +13,9 @@ use super::Engine;
1313use crate :: {
1414 config,
1515 task_graph:: TaskDefinition ,
16- turbo_json:: { validator:: Validator , ProcessedTaskDefinition , TurboJson , TurboJsonLoader } ,
16+ turbo_json:: {
17+ validator:: Validator , FutureFlags , ProcessedTaskDefinition , TurboJson , TurboJsonLoader ,
18+ } ,
1719} ;
1820
1921#[ derive( Debug , thiserror:: Error , Diagnostic ) ]
@@ -93,6 +95,16 @@ pub struct MissingTurboJsonExtends {
9395 text : NamedSource < String > ,
9496}
9597
98+ #[ derive( Debug , thiserror:: Error , Diagnostic ) ]
99+ #[ error( "Cyclic extends detected: {}" , cycle. join( " -> " ) ) ]
100+ pub struct CyclicExtends {
101+ cycle : Vec < String > ,
102+ #[ label( "Cycle detected here" ) ]
103+ span : Option < SourceSpan > ,
104+ #[ source_code]
105+ text : NamedSource < String > ,
106+ }
107+
96108#[ derive( Debug , thiserror:: Error , Diagnostic ) ]
97109pub enum Error {
98110 #[ error( "Missing tasks in project" ) ]
@@ -113,6 +125,9 @@ pub enum Error {
113125 MissingTurboJsonExtends ( Box < MissingTurboJsonExtends > ) ,
114126 #[ error( transparent) ]
115127 #[ diagnostic( transparent) ]
128+ CyclicExtends ( Box < CyclicExtends > ) ,
129+ #[ error( transparent) ]
130+ #[ diagnostic( transparent) ]
116131 Config ( #[ from] crate :: config:: Error ) ,
117132 #[ error( "Invalid turbo.json configuration" ) ]
118133 Validation {
@@ -137,6 +152,7 @@ pub struct EngineBuilder<'a> {
137152 tasks_only : bool ,
138153 add_all_tasks : bool ,
139154 should_validate_engine : bool ,
155+ validator : Validator ,
140156}
141157
142158impl < ' a > EngineBuilder < ' a > {
@@ -157,9 +173,15 @@ impl<'a> EngineBuilder<'a> {
157173 tasks_only : false ,
158174 add_all_tasks : false ,
159175 should_validate_engine : true ,
176+ validator : Validator :: new ( ) ,
160177 }
161178 }
162179
180+ pub fn with_future_flags ( mut self , future_flags : FutureFlags ) -> Self {
181+ self . validator = self . validator . with_future_flags ( future_flags) ;
182+ self
183+ }
184+
163185 pub fn with_tasks_only ( mut self , tasks_only : bool ) -> Self {
164186 self . tasks_only = tasks_only;
165187 self
@@ -591,8 +613,9 @@ impl<'a> EngineBuilder<'a> {
591613 task_name : & TaskName ,
592614 ) -> Result < Vec < ProcessedTaskDefinition > , Error > {
593615 let package_name = PackageName :: from ( task_id. package ( ) ) ;
594- let mut turbo_json_chain =
595- Self :: turbo_json_chain ( turbo_json_loader, & package_name) ?. into_iter ( ) ;
616+ let mut turbo_json_chain = self
617+ . turbo_json_chain ( turbo_json_loader, & package_name) ?
618+ . into_iter ( ) ;
596619 let mut task_definitions = Vec :: new ( ) ;
597620
598621 if let Some ( root_definition) = turbo_json_chain
@@ -619,7 +642,7 @@ impl<'a> EngineBuilder<'a> {
619642 } ;
620643 }
621644
622- if let Some ( turbo_json) = turbo_json_chain. next ( ) {
645+ for turbo_json in turbo_json_chain {
623646 if let Some ( workspace_def) = turbo_json. task ( task_id, task_name) ? {
624647 task_definitions. push ( workspace_def) ;
625648 }
@@ -643,10 +666,11 @@ impl<'a> EngineBuilder<'a> {
643666 // Provide the chain of turbo.json's to load to fully resolve all extends for a
644667 // package turbo.json.
645668 fn turbo_json_chain < ' b > (
669+ & self ,
646670 turbo_json_loader : & ' b TurboJsonLoader ,
647671 package_name : & PackageName ,
648672 ) -> Result < Vec < & ' b TurboJson > , Error > {
649- let validator = Validator :: new ( ) ;
673+ let validator = & self . validator ;
650674 let mut turbo_jsons = Vec :: with_capacity ( 2 ) ;
651675
652676 enum ReadReq {
@@ -673,10 +697,37 @@ impl<'a> EngineBuilder<'a> {
673697 }
674698 }
675699
676- let mut read_stack = vec ! [ ReadReq :: Infer ( package_name. clone( ) ) ] ;
700+ let mut read_stack = vec ! [ ( ReadReq :: Infer ( package_name. clone( ) ) , vec![ ] ) ] ;
701+ let mut visited = std:: collections:: HashSet :: new ( ) ;
677702
678- while let Some ( read_req) = read_stack. pop ( ) {
703+ while let Some ( ( read_req, mut path ) ) = read_stack. pop ( ) {
679704 let package_name = read_req. package_name ( ) ;
705+
706+ // Check for cycle by seeing if this package is already in the current path
707+ if let Some ( cycle_index) = path. iter ( ) . position ( |p : & PackageName | p == package_name) {
708+ // Found a cycle - build the cycle portion for error
709+ let mut cycle = path[ cycle_index..]
710+ . iter ( )
711+ . map ( |p| p. to_string ( ) )
712+ . collect :: < Vec < _ > > ( ) ;
713+ cycle. push ( package_name. to_string ( ) ) ;
714+
715+ let ( span, text) = read_req
716+ . required ( )
717+ . unwrap_or_else ( || ( None , NamedSource :: new ( "turbo.json" , String :: new ( ) ) ) ) ;
718+
719+ return Err ( Error :: CyclicExtends ( Box :: new ( CyclicExtends {
720+ cycle,
721+ span,
722+ text,
723+ } ) ) ) ;
724+ }
725+
726+ // Skip if we've already fully processed this package
727+ if visited. contains ( package_name) {
728+ continue ;
729+ }
730+
680731 let turbo_json = turbo_json_loader
681732 . load ( package_name)
682733 . map ( Some )
@@ -700,15 +751,23 @@ impl<'a> EngineBuilder<'a> {
700751 if let Some ( turbo_json) = turbo_json {
701752 Error :: from_validation ( validator. validate_turbo_json ( package_name, turbo_json) ) ?;
702753 turbo_jsons. push ( turbo_json) ;
754+ visited. insert ( package_name. clone ( ) ) ;
755+
756+ // Add current package to path for cycle detection
757+ path. push ( package_name. clone ( ) ) ;
758+
703759 // Add the new turbo.json we are extending from
704760 let ( extends, span) = turbo_json. extends . clone ( ) . split ( ) ;
705- for package_name in extends {
706- let package_name = PackageName :: from ( package_name) ;
707- read_stack. push ( ReadReq :: Request ( span. clone ( ) . to ( package_name) ) ) ;
761+ for extend_package in extends {
762+ let extend_package_name = PackageName :: from ( extend_package) ;
763+ read_stack. push ( (
764+ ReadReq :: Request ( span. clone ( ) . to ( extend_package_name) ) ,
765+ path. clone ( ) ,
766+ ) ) ;
708767 }
709768 } else if turbo_jsons. is_empty ( ) {
710769 // If there is no package turbo.json extend from root by default
711- read_stack. push ( ReadReq :: Infer ( PackageName :: Root ) ) ;
770+ read_stack. push ( ( ReadReq :: Infer ( PackageName :: Root ) , path ) ) ;
712771 }
713772 }
714773
@@ -1739,4 +1798,63 @@ mod test {
17391798 "../.."
17401799 ) ;
17411800 }
1801+
1802+ #[ test]
1803+ fn test_cyclic_extends ( ) {
1804+ let repo_root_dir = TempDir :: with_prefix ( "repo" ) . unwrap ( ) ;
1805+ let repo_root = AbsoluteSystemPathBuf :: new ( repo_root_dir. path ( ) . to_str ( ) . unwrap ( ) ) . unwrap ( ) ;
1806+ let package_graph = mock_package_graph (
1807+ & repo_root,
1808+ package_jsons ! {
1809+ repo_root,
1810+ "app1" => [ ] ,
1811+ "app2" => [ ]
1812+ } ,
1813+ ) ;
1814+
1815+ // Create a self-referencing cycle: Root extends itself
1816+ let turbo_jsons = vec ! [
1817+ (
1818+ PackageName :: Root ,
1819+ turbo_json( json!( {
1820+ "extends" : [ "//" ] , // Root extending itself creates a cycle
1821+ "tasks" : {
1822+ "build" : { }
1823+ }
1824+ } ) ) ,
1825+ ) ,
1826+ (
1827+ PackageName :: from( "app1" ) ,
1828+ turbo_json( json!( {
1829+ "extends" : [ "//" ] ,
1830+ "tasks" : { }
1831+ } ) ) ,
1832+ ) ,
1833+ (
1834+ PackageName :: from( "app2" ) ,
1835+ turbo_json( json!( {
1836+ "extends" : [ "//" ] ,
1837+ "tasks" : { }
1838+ } ) ) ,
1839+ ) ,
1840+ ]
1841+ . into_iter ( )
1842+ . collect ( ) ;
1843+
1844+ let loader = TurboJsonLoader :: noop ( turbo_jsons) ;
1845+ let engine_result = EngineBuilder :: new ( & repo_root, & package_graph, & loader, false )
1846+ . with_tasks ( Some ( Spanned :: new ( TaskName :: from ( "build" ) ) ) )
1847+ . with_workspaces ( vec ! [ PackageName :: from( "app1" ) ] )
1848+ . build ( ) ;
1849+
1850+ assert ! ( engine_result. is_err( ) ) ;
1851+ if let Err ( Error :: CyclicExtends ( box CyclicExtends { cycle, .. } ) ) = engine_result {
1852+ // The cycle should contain root (//) since it's a self-reference
1853+ assert ! ( cycle. contains( & "//" . to_string( ) ) ) ;
1854+ // Should have at least 2 entries to show the cycle (// -> //)
1855+ assert ! ( cycle. len( ) >= 2 ) ;
1856+ } else {
1857+ panic ! ( "Expected CyclicExtends error, got {:?}" , engine_result) ;
1858+ }
1859+ }
17421860}
0 commit comments