kotoba_formatter/
lib.rs

1//! Kotoba Code Formatter
2//!
3//! Denoの `deno fmt` に似た使い勝手で、.kotoba ファイルを
4//! 統一されたスタイルでフォーマットします。
5//!
6//! ## 使用方法
7//!
8//! ```bash
9//! # ファイルのフォーマット
10//! kotoba fmt file.kotoba
11//!
12//! # チェックのみ(変更しない)
13//! kotoba fmt --check file.kotoba
14//!
15//! # ディレクトリ内の全ファイルをフォーマット
16//! kotoba fmt .
17//! ```
18
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21use std::path::PathBuf;
22
23pub mod config;
24pub mod formatter;
25pub mod parser;
26pub mod rules;
27pub mod writer;
28
29/// Formatterの結果
30#[derive(Debug, Clone)]
31pub struct FormatResult {
32    /// 元のファイルパス
33    pub file_path: PathBuf,
34    /// フォーマット前の内容
35    pub original_content: String,
36    /// フォーマット後の内容
37    pub formatted_content: String,
38    /// 変更があったかどうか
39    pub has_changes: bool,
40    /// エラー(あれば)
41    pub error: Option<String>,
42}
43
44impl FormatResult {
45    /// 新しい結果を作成
46    pub fn new(file_path: PathBuf, original_content: String) -> Self {
47        Self {
48            file_path,
49            original_content: original_content.clone(),
50            formatted_content: original_content,
51            has_changes: false,
52            error: None,
53        }
54    }
55
56    /// フォーマット後の内容を設定
57    pub fn set_formatted_content(&mut self, content: String) {
58        self.has_changes = content != self.original_content;
59        self.formatted_content = content;
60    }
61
62    /// エラーを設定
63    pub fn set_error(&mut self, error: String) {
64        self.error = Some(error);
65    }
66}
67
68/// Formatterの設定
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct FormatterConfig {
71    /// インデントに使用する文字
72    pub indent_style: IndentStyle,
73    /// インデント幅
74    pub indent_width: usize,
75    /// 行の最大長
76    pub line_width: usize,
77    /// 改行スタイル
78    pub line_ending: LineEnding,
79    /// 波括弧のスタイル
80    pub brace_style: BraceStyle,
81    /// コンマの後ろにスペースを入れる
82    pub trailing_comma: bool,
83    /// 演算子の周りにスペースを入れる
84    pub space_around_operators: bool,
85    /// 空行の最大数
86    pub max_empty_lines: usize,
87}
88
89impl Default for FormatterConfig {
90    fn default() -> Self {
91        Self {
92            indent_style: IndentStyle::Space,
93            indent_width: 4,
94            line_width: 100,
95            line_ending: LineEnding::Lf,
96            brace_style: BraceStyle::SameLine,
97            trailing_comma: true,
98            space_around_operators: true,
99            max_empty_lines: 2,
100        }
101    }
102}
103
104/// インデントスタイル
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub enum IndentStyle {
107    /// スペース
108    Space,
109    /// タブ
110    Tab,
111}
112
113/// 改行スタイル
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub enum LineEnding {
116    /// LF (\n)
117    Lf,
118    /// CRLF (\r\n)
119    Crlf,
120    /// 自動検出
121    Auto,
122}
123
124/// 波括弧のスタイル
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub enum BraceStyle {
127    /// 同じ行に置く
128    SameLine,
129    /// 次の行に置く
130    NextLine,
131}
132
133/// Formatterのメイン構造体
134#[derive(Debug)]
135pub struct Formatter {
136    config: FormatterConfig,
137    rules: rules::FormatRules,
138}
139
140impl Formatter {
141    /// 新しいFormatterを作成
142    pub fn new(config: FormatterConfig) -> Self {
143        let rules = rules::FormatRules::new(&config);
144        Self { config, rules }
145    }
146
147    /// デフォルト設定でFormatterを作成
148    pub fn default() -> Self {
149        Self::new(FormatterConfig::default())
150    }
151
152    /// 設定ファイルからFormatterを作成
153    pub async fn from_config_file() -> Result<Self, Box<dyn std::error::Error>> {
154        let config = config::load_config().await?;
155        Ok(Self::new(config))
156    }
157
158    /// 単一ファイルをフォーマット
159    pub async fn format_file(&self, file_path: &PathBuf) -> Result<FormatResult, Box<dyn std::error::Error>> {
160        let content = tokio::fs::read_to_string(file_path).await?;
161        let mut result = FormatResult::new(file_path.clone(), content);
162
163        match self.format_content(&result.original_content).await {
164            Ok(formatted) => {
165                result.set_formatted_content(formatted);
166                Ok(result)
167            }
168            Err(e) => {
169                result.set_error(e.to_string());
170                Ok(result)
171            }
172        }
173    }
174
175    /// コンテンツをフォーマット
176    pub async fn format_content(&self, content: &str) -> Result<String, Box<dyn std::error::Error>> {
177        // 簡易的なフォーマット処理
178        // TODO: より洗練されたASTベースのフォーマットを実装
179
180        let mut lines = content.lines().collect::<Vec<_>>();
181        let mut formatted_lines = Vec::new();
182
183        for line in lines {
184            let formatted = self.format_line(line);
185            formatted_lines.push(formatted);
186        }
187
188        // 空行の整理
189        self.cleanup_empty_lines(&mut formatted_lines);
190
191        Ok(formatted_lines.join(&self.get_line_ending()))
192    }
193
194    /// 行をフォーマット
195    fn format_line(&self, line: &str) -> String {
196        let line = line.trim();
197
198        if line.is_empty() {
199            return String::new();
200        }
201
202        // 基本的な整形
203        let line = self.format_braces(line);
204        let line = self.format_operators(&line);
205        let line = self.format_commas(&line);
206
207        line
208    }
209
210    /// 波括弧をフォーマット
211    fn format_braces(&self, line: &str) -> String {
212        // 簡易実装
213        line.to_string()
214    }
215
216    /// 演算子をフォーマット
217    fn format_operators(&self, line: &str) -> String {
218        if !self.config.space_around_operators {
219            return line.to_string();
220        }
221
222        // 演算子の周りにスペースを入れる
223        line.replace("=", " = ")
224            .replace("==", " == ")
225            .replace("!=", " != ")
226            .replace("<", " < ")
227            .replace(">", " > ")
228            .replace("<=", " <= ")
229            .replace(">=", " >= ")
230            .replace("+", " + ")
231            .replace("-", " - ")
232            .replace("*", " * ")
233            .replace("/", " / ")
234            .replace("&&", " && ")
235            .replace("||", " || ")
236            .replace("->", " -> ")
237            .replace(":", " : ")
238            // 重複スペースを除去
239            .split_whitespace()
240            .collect::<Vec<_>>()
241            .join(" ")
242    }
243
244    /// コンマをフォーマット
245    fn format_commas(&self, line: &str) -> String {
246        if self.config.trailing_comma {
247            // コンマの後にスペースを入れる
248            line.replace(",", ", ")
249        } else {
250            line.to_string()
251        }
252    }
253
254    /// 空行を整理
255    fn cleanup_empty_lines(&self, lines: &mut Vec<String>) {
256        let mut result = Vec::new();
257        let mut empty_count = 0;
258
259        for line in &mut *lines {
260            if line.trim().is_empty() {
261                empty_count += 1;
262                if empty_count <= self.config.max_empty_lines {
263                    result.push(line.clone());
264                }
265            } else {
266                empty_count = 0;
267                result.push(line.clone());
268            }
269        }
270
271        *lines = result;
272    }
273
274    /// 改行文字を取得
275    fn get_line_ending(&self) -> String {
276        match self.config.line_ending {
277            LineEnding::Lf => "\n".to_string(),
278            LineEnding::Crlf => "\r\n".to_string(),
279            LineEnding::Auto => "\n".to_string(),
280        }
281    }
282}
283
284// 便利関数
285pub async fn format_files(files: Vec<PathBuf>, check_only: bool) -> Result<Vec<FormatResult>, Box<dyn std::error::Error>> {
286    let formatter = Formatter::default();
287    let mut results = Vec::new();
288
289    for file in files {
290        let result = formatter.format_file(&file).await?;
291        results.push(result);
292    }
293
294    Ok(results)
295}
296
297pub async fn format_directory(dir: PathBuf, check_only: bool) -> Result<Vec<FormatResult>, Box<dyn std::error::Error>> {
298    let mut files = Vec::new();
299
300    // .kotoba ファイルを再帰的に検索
301    find_kotoba_files(dir, &mut files).await?;
302
303    format_files(files, check_only).await
304}
305
306/// .kotoba ファイルを再帰的に検索
307async fn find_kotoba_files(dir: PathBuf, files: &mut Vec<PathBuf>) -> Result<(), Box<dyn std::error::Error>> {
308    let mut entries = tokio::fs::read_dir(&dir).await?;
309
310    while let Some(entry) = entries.next_entry().await? {
311        let path = entry.path();
312
313        if path.is_dir() {
314            // node_modules や .git はスキップ
315            if !path.ends_with("node_modules") && !path.ends_with(".git") {
316                Box::pin(find_kotoba_files(path, files)).await?;
317            }
318        } else if path.extension().map_or(false, |ext| ext == "kotoba") {
319            files.push(path);
320        }
321    }
322
323    Ok(())
324}
325
326// 各モジュールの再エクスポート
327pub use config::*;
328pub use formatter::*;
329pub use parser::*;
330pub use rules::*;
331pub use writer::*;