ezno_checker/features/
modules.rs

1use std::path::{Path, PathBuf};
2
3use super::variables::{VariableMutability, VariableOrImport};
4use crate::{
5	context::{
6		information::{get_value_of_constant_import_variable, LocalInformation},
7		VariableRegisterArguments,
8	},
9	parse_source, CheckingData, Environment, Instance, Map, Scope, TypeId, TypeMappings,
10	VariableId,
11};
12
13use simple_json_parser::{JSONKey, RootJSONValue};
14use source_map::{FileSystem, Span};
15
16/// For imports and exports
17#[derive(Debug)]
18pub struct NamePair<'a> {
19	pub value: &'a str,
20	pub r#as: &'a str,
21	pub position: Span,
22}
23
24pub enum ImportKind<'a, T: Iterator<Item = NamePair<'a>>> {
25	Parts(T),
26	All {
27		under: &'a str,
28		position: Span,
29	},
30	/// From `export * from ...`
31	Everything,
32}
33
34/// A module once it has been type checked (note could have type errors that have been raised) and all information has been resolved about it
35pub struct SynthesisedModule<M> {
36	pub content: M,
37	pub exported: Exported,
38	/// TODO ...
39	pub info: LocalInformation,
40	pub mappings: TypeMappings,
41}
42
43impl<M> SynthesisedModule<M> {
44	pub fn get_instance_at_position(&self, pos: u32) -> Option<&Instance> {
45		self.mappings.expressions_to_instances.get(pos)
46	}
47
48	/// Returns the instance + the span of the matched
49	pub fn get_instance_at_position_with_span(
50		&self,
51		pos: u32,
52	) -> Option<(&Instance, std::ops::Range<u32>)> {
53		self.mappings.expressions_to_instances.get_with_range(pos)
54	}
55}
56
57/// TODO tidy
58#[derive(Clone, Debug, Default, binary_serialize_derive::BinarySerializable)]
59pub struct Exported {
60	pub default: Option<TypeId>,
61	/// Mutability purely for the mutation thingy
62	pub named: Map<String, (VariableId, VariableMutability)>,
63	pub named_types: Map<String, TypeId>,
64}
65
66pub type ExportedVariable = (VariableId, VariableMutability);
67
68impl Exported {
69	#[must_use]
70	pub fn get_export(
71		&self,
72		want: &str,
73		type_only: bool,
74	) -> (Option<ExportedVariable>, Option<TypeId>) {
75		let variable = if type_only {
76			None
77		} else {
78			self.named.get(want).map(|(name, mutability)| (*name, *mutability))
79		};
80
81		let r#type = self.named_types.get(want).copied();
82
83		(variable, r#type)
84	}
85
86	pub fn keys(&self) -> impl Iterator<Item = &str> {
87		self.named.keys().chain(self.named_types.keys()).map(AsRef::as_ref)
88	}
89}
90
91/// After a syntax error
92pub struct InvalidModule;
93
94/// The result of synthesising a module
95pub type FinalModule<M> = Result<SynthesisedModule<M>, InvalidModule>;
96
97#[derive(Debug, Clone)]
98pub struct CouldNotOpenFile(pub PathBuf);
99
100/// Given the current environment for a module:
101/// - Parse the `partial_import_path` to a resolved module
102/// - Type check the module (if not already done)
103/// - Import names by `kind` (and another information)
104#[allow(clippy::too_many_arguments)]
105pub fn import_items<
106	'b,
107	P: Iterator<Item = NamePair<'b>>,
108	T: crate::ReadFromFS,
109	A: crate::ASTImplementation,
110>(
111	environment: &mut Environment,
112	partial_import_path: &str,
113	import_position: Span,
114	default_import: Option<(&str, Span)>,
115	kind: ImportKind<'b, P>,
116	checking_data: &mut CheckingData<T, A>,
117	also_export: bool,
118	type_only: bool,
119) {
120	if !matches!(environment.context_type.scope, crate::Scope::Module { .. }) {
121		checking_data.diagnostics_container.add_error(
122			crate::diagnostics::TypeCheckError::NotTopLevelImport(
123				import_position.with_source(environment.get_source()),
124			),
125		);
126		return;
127	}
128
129	let exports = import_file(partial_import_path, environment, checking_data);
130
131	if let Err(ref err) = exports {
132		checking_data.diagnostics_container.add_error(
133			crate::diagnostics::TypeCheckError::CannotOpenFile {
134				file: err.clone(),
135				import_position: Some(import_position.with_source(environment.get_source())),
136				possibles: checking_data
137					.modules
138					.files
139					.get_paths()
140					.keys()
141					.filter_map(|path| path.to_str())
142					.collect(),
143				partial_import_path,
144			},
145		);
146	}
147
148	let current_source = environment.get_source();
149
150	if let Some((default_name, position)) = default_import {
151		if let Ok(Ok(ref exports)) = exports {
152			if let Some(item) = &exports.default {
153				let id = crate::VariableId(current_source, position.start);
154				let v = VariableOrImport::ConstantImport {
155					to: None,
156					import_specified_at: position.with_source(current_source),
157				};
158				environment.info.variable_current_value.insert(id, *item);
159				let existing = environment.variables.insert(default_name.to_owned(), v);
160				if let Some(existing) = existing {
161					checking_data.diagnostics_container.add_error(
162						crate::diagnostics::TypeCheckError::DuplicateImportName {
163							import_position: position.with_source(current_source),
164							existing_position: match existing {
165								VariableOrImport::Variable { declared_at, .. } => declared_at,
166								VariableOrImport::MutableImport { import_specified_at, .. }
167								| VariableOrImport::ConstantImport {
168									import_specified_at, ..
169								} => import_specified_at,
170							},
171						},
172					);
173				}
174			} else {
175				checking_data.diagnostics_container.add_error(
176					crate::diagnostics::TypeCheckError::NoDefaultExport {
177						position: position.with_source(current_source),
178						partial_import_path,
179					},
180				);
181			}
182		} else {
183			environment.register_variable_handle_error(
184				default_name,
185				VariableRegisterArguments {
186					constant: true,
187					initial_value: Some(TypeId::ERROR_TYPE),
188					space: None,
189					allow_reregistration: false,
190				},
191				position.with_source(current_source),
192				&mut checking_data.diagnostics_container,
193				&mut checking_data.local_type_mappings,
194				checking_data.options.record_all_assignments_and_reads,
195			);
196		}
197	}
198
199	match kind {
200		ImportKind::Parts(parts) => {
201			for part in parts {
202				// Here in nested because want to type variables as error otherwise
203				if let Ok(Ok(ref exports)) = exports {
204					crate::utilities::notify!("{:?}", part);
205					let (exported_variable, exported_type) =
206						exports.get_export(part.value, type_only);
207
208					if exported_variable.is_none() && exported_type.is_none() {
209						let possibles = {
210							let mut possibles =
211								crate::get_closest(exports.keys(), part.value).unwrap_or(vec![]);
212							possibles.sort_unstable();
213							possibles
214						};
215						let position = part.position.with_source(current_source);
216						checking_data.diagnostics_container.add_error(
217							crate::diagnostics::TypeCheckError::FieldNotExported {
218								file: partial_import_path,
219								position,
220								importing: part.value,
221								possibles,
222							},
223						);
224
225						// Register error
226						environment.register_variable_handle_error(
227							part.r#as,
228							VariableRegisterArguments {
229								constant: true,
230								space: None,
231								initial_value: Some(TypeId::ERROR_TYPE),
232								allow_reregistration: false,
233							},
234							position,
235							&mut checking_data.diagnostics_container,
236							&mut checking_data.local_type_mappings,
237							checking_data.options.record_all_assignments_and_reads,
238						);
239					}
240
241					// add variable to scope
242					if let Some((variable, mutability)) = exported_variable {
243						let constant = match mutability {
244							VariableMutability::Constant => {
245								let k = crate::VariableId(current_source, part.position.start);
246								let v =
247									get_value_of_constant_import_variable(variable, environment);
248								environment.info.variable_current_value.insert(k, v);
249								true
250							}
251							VariableMutability::Mutable { reassignment_constraint: _ } => false,
252						};
253
254						let v = VariableOrImport::MutableImport {
255							of: variable,
256							constant,
257							import_specified_at: part
258								.position
259								.with_source(environment.get_source()),
260						};
261						crate::utilities::notify!("{:?}", part.r#as.to_owned());
262						let existing = environment.variables.insert(part.r#as.to_owned(), v);
263						if let Some(existing) = existing {
264							checking_data.diagnostics_container.add_error(
265								crate::diagnostics::TypeCheckError::DuplicateImportName {
266									import_position: part
267										.position
268										.with_source(environment.get_source()),
269									existing_position: match existing {
270										VariableOrImport::Variable { declared_at, .. } => {
271											declared_at
272										}
273										VariableOrImport::MutableImport {
274											import_specified_at,
275											..
276										}
277										| VariableOrImport::ConstantImport {
278											import_specified_at,
279											..
280										} => import_specified_at,
281									},
282								},
283							);
284						}
285						if also_export {
286							if let Scope::Module { ref mut exported, .. } =
287								environment.context_type.scope
288							{
289								exported.named.insert(part.r#as.to_owned(), (variable, mutability));
290							}
291						}
292					}
293
294					// add type to scope
295					if let Some(ty) = exported_type {
296						let existing = environment.named_types.insert(part.r#as.to_owned(), ty);
297						assert!(existing.is_none(), "TODO exception");
298					}
299				} else {
300					// This happens if imported is an invalid file (syntax issue, doesn't exist etc)
301					// Don't need to emit an error here
302					let declared_at = part.position.with_source(environment.get_source());
303					environment.register_variable_handle_error(
304						part.r#as,
305						VariableRegisterArguments {
306							constant: true,
307							space: None,
308							initial_value: Some(TypeId::ERROR_TYPE),
309							allow_reregistration: false,
310						},
311						declared_at,
312						&mut checking_data.diagnostics_container,
313						&mut checking_data.local_type_mappings,
314						checking_data.options.record_all_assignments_and_reads,
315					);
316				}
317			}
318		}
319		ImportKind::All { under, position } => {
320			let value = if let Ok(Ok(ref exports)) = exports {
321				let import_object = crate::Type::SpecialObject(
322					crate::features::objects::SpecialObject::Import(exports.clone()),
323				);
324				checking_data.types.register_type(import_object)
325			} else {
326				crate::utilities::notify!("TODO :?");
327				TypeId::UNIMPLEMENTED_ERROR_TYPE
328			};
329			environment.register_variable_handle_error(
330				under,
331				VariableRegisterArguments {
332					constant: true,
333					space: None,
334					initial_value: Some(value),
335					allow_reregistration: false,
336				},
337				position.with_source(current_source),
338				&mut checking_data.diagnostics_container,
339				&mut checking_data.local_type_mappings,
340				checking_data.options.record_all_assignments_and_reads,
341			);
342		}
343		ImportKind::Everything => {
344			if let Ok(Ok(ref exports)) = exports {
345				for (name, (variable, mutability)) in exports.named.iter() {
346					// TODO are variables put into scope?
347					if let Scope::Module { ref mut exported, .. } = environment.context_type.scope {
348						exported.named.insert(name.clone(), (*variable, *mutability));
349					}
350				}
351			} else {
352				// TODO ??
353			}
354		}
355	}
356}
357
358/// TODO better name.
359pub fn import_file<T: crate::ReadFromFS, A: crate::ASTImplementation>(
360	to_import: &str,
361	environment: &mut Environment,
362	checking_data: &mut CheckingData<T, A>,
363) -> Result<Result<Exported, InvalidModule>, CouldNotOpenFile> {
364	fn get_module<'a, T: crate::ReadFromFS, A: crate::ASTImplementation>(
365		full_importer: &Path,
366		_definition_file: Option<&Path>,
367		environment: &mut Environment,
368		checking_data: &'a mut CheckingData<T, A>,
369	) -> Option<Result<&'a SynthesisedModule<A::OwnedModule>, A::ParseError>> {
370		let existing = checking_data.modules.files.get_source_at_path(full_importer);
371		if let Some(existing) = existing {
372			Some(Ok(checking_data
373				.modules
374				.synthesised_modules
375				.get(&existing)
376				.expect("existing file, but not synthesised")))
377		} else {
378			let content = checking_data.modules.file_reader.read_file(full_importer);
379			if let Some(content) = content {
380				let content = String::from_utf8(content).expect("invalid entry point encoding");
381				let source = checking_data
382					.modules
383					.files
384					.new_source_id(full_importer.to_path_buf(), content.clone());
385				let module = parse_source(full_importer, source, content, checking_data);
386
387				match module {
388					Ok(module) => {
389						let root = &environment.get_root();
390						let new_module_context =
391							root.new_module_context(source, module, checking_data);
392						Some(Ok(new_module_context))
393					}
394					Err(err) => Some(Err(err)),
395				}
396			} else {
397				None
398			}
399		}
400	}
401
402	fn get_package_from_node_modules<T: crate::ReadFromFS>(
403		name: &str,
404		cwd: &Path,
405		fs_reader: &T,
406	) -> Result<(PathBuf, Option<PathBuf>), ()> {
407		// TODO support non `node_modules` or is that over ?
408		let package_directory = cwd.join("node_modules");
409		let package_root = package_directory.join(name);
410		let package_json_path = package_root.join("package.json");
411		// TODO error
412		let package_json = fs_reader.read_file(&PathBuf::from(&package_json_path)).ok_or(())?;
413		let package_json = String::from_utf8(package_json).unwrap();
414
415		let (mut file_path, mut definition_file_path) = (None::<PathBuf>, None::<PathBuf>);
416
417		// TODO JSON parse error
418		let _res = simple_json_parser::parse_with_exit_signal(&package_json, |path, value| {
419			// if let Some(ref export) = export {
420			// 	todo!()
421			// } else {
422			if let [JSONKey::Slice("main")] = path {
423				if let RootJSONValue::String(s) = value {
424					file_path = Some(s.to_owned().into());
425				} else {
426					// invalid type
427				}
428			} else if let [JSONKey::Slice("types")] = path {
429				if let RootJSONValue::String(s) = value {
430					definition_file_path = Some(s.to_owned().into());
431				} else {
432					// invalid type
433				}
434			}
435			// }
436			file_path.is_some() && definition_file_path.is_some()
437		});
438
439		file_path.ok_or(()).map(|entry| {
440			(package_root.join(entry), definition_file_path.map(|dfp| package_root.join(dfp)))
441		})
442	}
443
444	let result = if to_import.starts_with('.') {
445		let from_path = checking_data.modules.files.get_file_path(environment.get_source());
446		let from = PathBuf::from(to_import);
447		let mut full_importer =
448			path_absolutize::Absolutize::absolutize_from(&from, from_path.parent().unwrap())
449				.unwrap()
450				.to_path_buf();
451
452		if full_importer.extension().is_some() {
453			get_module(&full_importer, None, environment, checking_data)
454		} else {
455			let mut result = None;
456			for ext in ["ts", "tsx", "js"] {
457				full_importer.set_extension(ext);
458				// TODO change parse options based on extension
459				result = get_module(&full_importer, None, environment, checking_data);
460				if result.is_some() {
461					break;
462				}
463			}
464			result
465		}
466	} else {
467		crate::utilities::notify!("Here {}", to_import);
468		let result = get_package_from_node_modules(
469			to_import,
470			&checking_data.modules.current_working_directory,
471			checking_data.modules.file_reader,
472		);
473		if let Ok((path, definition_file)) = result {
474			crate::utilities::notify!("Reading path from package {}", path.display());
475			get_module(&path, definition_file.as_deref(), environment, checking_data)
476		} else {
477			None
478		}
479	};
480
481	match result {
482		Some(Ok(synthesised_module)) => {
483			environment.info.extend_ref(&synthesised_module.info);
484			Ok(Ok(synthesised_module.exported.clone()))
485		}
486		Some(Err(error)) => {
487			checking_data.diagnostics_container.add_error(error);
488			Ok(Err(InvalidModule))
489		}
490		None => Err(CouldNotOpenFile(PathBuf::from(to_import.to_owned()))),
491	}
492}
493
494// pub fn get_possibles_message_for_imports(possibles: &[&str]) -> Vec<String> {
495// 	possibles
496// 		.iter()
497// 		.filter(|file| !file.ends_with(".d.ts"))
498// 		.filter_map(|file| file.strip_suffix(".ts"))
499// 		.map(|file| {
500// 			if file.starts_with("./") || file.starts_with("../") {
501// 				file.to_string()
502// 			} else {
503// 				"./".to_string() + file
504// 			}
505// 		})
506// 		.collect::<Vec<String>>()
507// }