@@ -28,6 +28,136 @@ def parse_command(args):
2828 sys .exit (1 )
2929
3030
31+ def _get_file_dependencies (source_file : Path , parser ) -> tuple [str , set [str ]]:
32+ """Extract package and imported types from a source file.
33+ Returns: (package_name, set_of_imported_types)"""
34+ ast = parser .parse_file (str (source_file ))
35+
36+ package = ast .package .name if ast .package else ""
37+ imported_types = set ()
38+
39+ # Collect imported types (simple names only, from same or imported packages)
40+ for imp in ast .imports :
41+ if not imp .is_static and not imp .is_wildcard :
42+ # Single-type import: java.util.List -> List
43+ simple_name = imp .name .split ("." )[- 1 ]
44+ imported_types .add (simple_name )
45+
46+ return package , imported_types
47+
48+
49+ def _topological_sort (files : list [Path ], parser ) -> list [Path ]:
50+ """Sort files in dependency order using topological sort.
51+ Files that define types used by other files come first."""
52+ from collections import defaultdict , deque
53+
54+ # Map: package.TypeName -> source file
55+ type_to_file = {}
56+ # Map: source file -> set of types it depends on
57+ file_deps = {}
58+
59+ # First pass: identify what types each file defines
60+ for f in files :
61+ ast = parser .parse_file (str (f ))
62+ package = ast .package .name if ast .package else ""
63+
64+ for type_decl in ast .types :
65+ type_name = type_decl .name
66+ full_name = f"{ package } .{ type_name } " if package else type_name
67+ type_to_file [full_name ] = f
68+
69+ # Second pass: identify dependencies
70+ for f in files :
71+ ast = parser .parse_file (str (f ))
72+ package = ast .package .name if ast .package else ""
73+ deps = set ()
74+
75+ # Add dependencies from imports
76+ for imp in ast .imports :
77+ if not imp .is_static and not imp .is_wildcard :
78+ # Check if this import is for a type in our compilation set
79+ imported_type = imp .name
80+ if imported_type in type_to_file :
81+ deps .add (imported_type )
82+ else :
83+ deps .add (imp .name )
84+
85+ # For same-package dependencies, check if type names appear in source
86+ # This is a heuristic for detecting usage without full semantic analysis
87+ source_text = f .read_text ()
88+ for full_type_name , type_file in type_to_file .items ():
89+ if type_file != f : # Don't depend on ourselves
90+ simple_name = full_type_name .split ("." )[- 1 ]
91+ # Check if type name appears in source (crude but effective)
92+ if package :
93+ # Same package?
94+ type_package = "." .join (full_type_name .split ("." )[:- 1 ])
95+ if type_package == package and simple_name in source_text :
96+ deps .add (full_type_name )
97+
98+ # Check if types defined in this file extend/implement types in other files
99+ for type_decl in ast .types :
100+ from pyjopa .ast import ClassDeclaration , InterfaceDeclaration , EnumDeclaration
101+
102+ # Get superclass and interfaces
103+ if isinstance (type_decl , ClassDeclaration ) and type_decl .extends :
104+ # extends Type -> might be in same package
105+ super_name = type_decl .extends .name if hasattr (type_decl .extends , 'name' ) else str (type_decl .extends )
106+ # Try same package first
107+ if package :
108+ candidate = f"{ package } .{ super_name } "
109+ if candidate in type_to_file :
110+ deps .add (candidate )
111+ else :
112+ if super_name in type_to_file :
113+ deps .add (super_name )
114+
115+ if isinstance (type_decl , (ClassDeclaration , EnumDeclaration )):
116+ for iface in type_decl .implements :
117+ iface_name = iface .name if hasattr (iface , 'name' ) else str (iface )
118+ if package :
119+ candidate = f"{ package } .{ iface_name } "
120+ if candidate in type_to_file :
121+ deps .add (candidate )
122+ else :
123+ if iface_name in type_to_file :
124+ deps .add (iface_name )
125+
126+ file_deps [f ] = deps
127+
128+ # Build adjacency list: file -> files that depend on it
129+ in_degree = {f : 0 for f in files }
130+ adj = defaultdict (list )
131+
132+ for file , deps in file_deps .items ():
133+ for dep in deps :
134+ if dep in type_to_file :
135+ dep_file = type_to_file [dep ]
136+ if dep_file != file : # Skip self-dependencies
137+ adj [dep_file ].append (file )
138+ in_degree [file ] += 1
139+
140+ # Topological sort using Kahn's algorithm
141+ queue = deque ([f for f in files if in_degree [f ] == 0 ])
142+ result = []
143+
144+ while queue :
145+ current = queue .popleft ()
146+ result .append (current )
147+
148+ for neighbor in adj [current ]:
149+ in_degree [neighbor ] -= 1
150+ if in_degree [neighbor ] == 0 :
151+ queue .append (neighbor )
152+
153+ # Check for cycles
154+ if len (result ) != len (files ):
155+ # Circular dependency detected - return original order
156+ return files
157+
158+ return result
159+
160+
31161def compile_command (args ):
32162 """Compile Java files to .class bytecode."""
33163 from .parser import Java8Parser
@@ -43,15 +173,36 @@ def compile_command(args):
43173 classpath .add_rt_jar ()
44174 except FileNotFoundError :
45175 print ("Warning: rt.jar not found, method resolution may be limited" , file = sys .stderr )
176+ else :
177+ classpath = ClassPath ()
46178
47179 output_dir = Path (args .output ) if args .output else Path ("." )
180+
181+ # Create output directory first
48182 output_dir .mkdir (parents = True , exist_ok = True )
49183
184+ # Add custom classpath entries
185+ if args .classpath :
186+ import os
187+ for entry in args .classpath .split (os .pathsep ):
188+ if entry :
189+ classpath .add_path (entry )
190+
191+ # Add output directory to classpath so previously compiled classes can be found
192+ if classpath :
193+ classpath .add_path (str (output_dir .absolute ()))
194+
195+ # Sort files in dependency order if multiple files
196+ file_paths = [Path (f ) for f in args .files ]
197+ if len (file_paths ) > 1 :
198+ file_paths = _topological_sort (file_paths , parser )
199+ if args .verbose :
200+ print (f"Compilation order: { [str (f ) for f in file_paths ]} " )
201+
50202 total_classes = 0
51- for source_file in args .files :
52- path = Path (source_file )
203+ for path in file_paths :
53204 if not path .exists ():
54- print (f"Error: File not found: { source_file } " , file = sys .stderr )
205+ print (f"Error: File not found: { path } " , file = sys .stderr )
55206 sys .exit (1 )
56207
57208 try :
@@ -61,21 +212,22 @@ def compile_command(args):
61212
62213 for name , bytecode in class_files .items ():
63214 class_path = output_dir / f"{ name } .class"
215+ class_path .parent .mkdir (parents = True , exist_ok = True )
64216 with open (class_path , "wb" ) as f :
65217 f .write (bytecode )
66218 if args .verbose :
67219 print (f"Wrote { class_path } " )
68220 total_classes += 1
69221
70222 except Exception as e :
71- print (f"Error compiling { source_file } : { e } " , file = sys .stderr )
223+ print (f"Error compiling { path } : { e } " , file = sys .stderr )
72224 sys .exit (1 )
73225
74226 if classpath :
75227 classpath .close ()
76228
77229 if not args .quiet :
78- print (f"Compiled { len (args . files )} file(s) to { total_classes } class(es)" )
230+ print (f"Compiled { len (file_paths )} file(s) to { total_classes } class(es)" )
79231
80232
81233def main ():
@@ -113,6 +265,10 @@ def main():
113265 "-o" , "--output" ,
114266 help = "Output directory for .class files (default: current directory)" ,
115267 )
268+ compile_parser .add_argument (
269+ "-cp" , "--classpath" ,
270+ help = "Additional classpath entries (colon-separated paths to .jar files or directories)" ,
271+ )
116272 compile_parser .add_argument (
117273 "--no-rt" ,
118274 action = "store_true" ,
0 commit comments