chore: project skeleton — module contracts, docs plan, GPLv3
This commit is contained in:
commit
a33ed2eb30
20 changed files with 2220 additions and 0 deletions
35
src/core/apply.rs
Normal file
35
src/core/apply.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//! Atomic apply with rollback, plus unified-diff rendering for dry-run.
|
||||
//!
|
||||
//! Order matters: specifier rewrites are applied to importer files first
|
||||
//! (each written atomically via temp-file + rename), and the actual
|
||||
//! `source -> target` rename happens last. Any failure mid-way triggers
|
||||
//! rollback of everything already written.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::core::JmoveResult;
|
||||
use crate::core::plan::MovePlan;
|
||||
|
||||
/// Summary of a successfully applied plan.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Applied {
|
||||
/// Number of files whose imports were rewritten.
|
||||
pub files_rewritten: usize,
|
||||
/// The moved file's new project-relative path.
|
||||
pub new_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
/// Apply `plan` under `root` atomically (see module docs). Rollback is
|
||||
/// best-effort: on restore failure the error message states which files
|
||||
/// need manual recovery.
|
||||
pub fn apply(root: &Path, plan: &MovePlan) -> JmoveResult<Applied> {
|
||||
let _ = (root, plan);
|
||||
todo!("index agent: atomic apply + rollback")
|
||||
}
|
||||
|
||||
/// Render the plan as a unified diff (rewrites + file rename) for dry-run.
|
||||
#[must_use]
|
||||
pub fn render_diff(root: &Path, plan: &MovePlan) -> JmoveResult<String> {
|
||||
let _ = (root, plan);
|
||||
todo!("index agent: diff rendering via `similar`")
|
||||
}
|
||||
79
src/core/index.rs
Normal file
79
src/core/index.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
//! Project indexing: gitignore-aware filesystem scan plus the import graph.
|
||||
//!
|
||||
//! Built fresh on every command (disk cache is Phase 2). `ignore::WalkBuilder`
|
||||
//! handles `.gitignore`/hidden-file rules; every indexed TS/JS source file is
|
||||
//! parsed through [`crate::parser`] and its specifiers resolved through
|
||||
//! [`crate::parser::resolve`].
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::core::JmoveResult;
|
||||
use crate::parser::ImportRecord;
|
||||
|
||||
/// Indexed source files with O(1) membership lookups.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FileSet {
|
||||
paths: HashSet<PathBuf>,
|
||||
}
|
||||
|
||||
impl FileSet {
|
||||
/// Add a normalized project-relative path; `false` if already present.
|
||||
pub fn add(&mut self, path: PathBuf) -> bool {
|
||||
self.paths.insert(path)
|
||||
}
|
||||
|
||||
/// Whether `path` is a known indexed source file.
|
||||
#[must_use]
|
||||
pub fn contains(&self, path: &Path) -> bool {
|
||||
self.paths.contains(path)
|
||||
}
|
||||
|
||||
/// All files in deterministic sorted order (stable for tests and diffs).
|
||||
#[must_use]
|
||||
pub fn sorted(&self) -> Vec<PathBuf> {
|
||||
let mut v: Vec<PathBuf> = self.paths.iter().cloned().collect();
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
/// One import occurrence plus the project file it resolves to.
|
||||
/// `target: None` means "external" — a bare package specifier or a path
|
||||
/// that does not exist in the index.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResolvedImport {
|
||||
/// Raw record from the parser (specifier text + byte span).
|
||||
pub record: ImportRecord,
|
||||
/// Project-relative resolved file, if any.
|
||||
pub target: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Full in-memory project index: file set and forward import edges.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Index {
|
||||
/// Absolute project root the index was built for.
|
||||
pub root: PathBuf,
|
||||
/// All indexed source files.
|
||||
pub files: FileSet,
|
||||
/// For each file, the imports it declares (in source order).
|
||||
pub imports: HashMap<PathBuf, Vec<ResolvedImport>>,
|
||||
}
|
||||
|
||||
impl Index {
|
||||
/// Scan `root`, parse every supported source file and build the graph.
|
||||
/// Unparseable files are skipped, not fatal.
|
||||
pub fn build(root: &Path) -> JmoveResult<Self> {
|
||||
let _ = root;
|
||||
todo!(
|
||||
"index agent: scan with `ignore`, parse via crate::parser, resolve via parser::resolve"
|
||||
)
|
||||
}
|
||||
|
||||
/// Reverse edge lookup: every indexed file that imports `target`.
|
||||
#[must_use]
|
||||
pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
|
||||
let _ = target;
|
||||
todo!("index agent: reverse-edge lookup")
|
||||
}
|
||||
}
|
||||
103
src/core/mod.rs
Normal file
103
src/core/mod.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
//! Core engine: project indexing, dependency graph, move planning and
|
||||
//! atomic apply with rollback.
|
||||
//!
|
||||
//! Path convention used across the crate: every `PathBuf` produced by
|
||||
//! `jmove` is **project-root-relative, normalized** (no `.`/`..` segments).
|
||||
//! Use [`normalize_rel_path`] to canonicalize paths coming from users or
|
||||
//! from OS walking.
|
||||
|
||||
pub mod apply;
|
||||
pub mod index;
|
||||
pub mod plan;
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Crate-wide error type surfaced to the CLI layer.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum JmoveError {
|
||||
/// Filesystem or IO failure.
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
/// The user supplied a path that is invalid for the requested operation.
|
||||
#[error("invalid argument: {0}")]
|
||||
InvalidArgument(String),
|
||||
/// Project index is stale (a file vanished or was moved externally).
|
||||
#[error("index is stale: {0}")]
|
||||
StaleIndex(String),
|
||||
/// The planned move cannot be applied safely.
|
||||
#[error("plan rejected: {0}")]
|
||||
PlanRejected(String),
|
||||
}
|
||||
|
||||
/// Result alias used throughout the crate.
|
||||
pub type JmoveResult<T> = Result<T, JmoveError>;
|
||||
|
||||
/// Normalize a project-relative path: strip `.` segments, collapse `..`
|
||||
/// where possible and reject paths that escape the project root.
|
||||
/// Returns `None` if the result would be empty, absolute or above the root.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::path::Path;
|
||||
/// use jmove::core::normalize_rel_path;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// normalize_rel_path(Path::new("./src/../utils/foo.ts")),
|
||||
/// Some(PathBuf::from("utils/foo.ts"))
|
||||
/// );
|
||||
/// assert_eq!(normalize_rel_path(Path::new("../outside")), None);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn normalize_rel_path(path: &Path) -> Option<PathBuf> {
|
||||
let mut stack: Vec<&OsStr> = Vec::new();
|
||||
for comp in path.components() {
|
||||
match comp {
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => {
|
||||
if stack.pop().is_none() {
|
||||
return None; // would escape the project root
|
||||
}
|
||||
}
|
||||
Component::Normal(piece) => stack.push(piece),
|
||||
// Absolute paths and Windows prefixes are not project-relative.
|
||||
Component::RootDir | Component::Prefix(_) => return None,
|
||||
}
|
||||
}
|
||||
(!stack.is_empty()).then(|| stack.iter().collect::<PathBuf>())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::normalize_rel_path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[test]
|
||||
fn normalizes_dots_and_parent_dirs() {
|
||||
assert_eq!(
|
||||
normalize_rel_path(Path::new("./src/../utils/foo.ts")),
|
||||
Some(PathBuf::from("utils/foo.ts"))
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_rel_path(Path::new("a/b/c/../../d.ts")),
|
||||
Some(PathBuf::from("a/d.ts"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_escaping_and_empty_paths() {
|
||||
assert_eq!(normalize_rel_path(Path::new("../outside")), None);
|
||||
assert_eq!(normalize_rel_path(Path::new("a/../../outside")), None);
|
||||
assert_eq!(normalize_rel_path(Path::new("")), None);
|
||||
assert_eq!(normalize_rel_path(Path::new("./")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_absolute_paths() {
|
||||
assert_eq!(normalize_rel_path(Path::new("/etc/passwd")), None);
|
||||
}
|
||||
}
|
||||
46
src/core/plan.rs
Normal file
46
src/core/plan.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
//! Move planning: decide which import specifiers must be rewritten.
|
||||
//!
|
||||
//! A plan is pure data (no disk writes), so dry-run and `--json` can render
|
||||
//! it without touching the filesystem.
|
||||
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::core::JmoveResult;
|
||||
use crate::core::index::Index;
|
||||
|
||||
/// One in-file replacement of an import specifier. Only the specifier text
|
||||
/// between the quotes is touched — the statement layout is never reformatted.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Rewrite {
|
||||
/// Project-relative file to modify.
|
||||
pub file: PathBuf,
|
||||
/// Byte range of the old specifier text (without quotes).
|
||||
pub span: Range<usize>,
|
||||
/// Specifier as currently written.
|
||||
pub old_text: String,
|
||||
/// Specifier after the move.
|
||||
pub new_text: String,
|
||||
}
|
||||
|
||||
/// Complete plan for moving `source` to `target`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MovePlan {
|
||||
/// Project-relative path being moved.
|
||||
pub source: PathBuf,
|
||||
/// Project-relative destination path.
|
||||
pub target: PathBuf,
|
||||
/// Specifier rewrites, grouped per importer file.
|
||||
pub rewrites: Vec<Rewrite>,
|
||||
}
|
||||
|
||||
/// Compute the rewrite plan for `source -> target`.
|
||||
///
|
||||
/// Every indexed import whose resolved target is `source` gets a new
|
||||
/// relative specifier computed from the *importer's* directory to `target`.
|
||||
/// Rewrites whose result equals the old specifier are dropped.
|
||||
pub fn plan_move(index: &Index, source: &Path, target: &Path) -> JmoveResult<MovePlan> {
|
||||
let _ = (index, source, target);
|
||||
todo!("index agent: implement planner incl. relative-specifier math")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue