1+ use ruff_db:: files:: File ;
2+ use ruff_db:: parsed:: parsed_module;
3+ use ruff_db:: source:: source_text;
14use thiserror:: Error ;
25use tracing:: Level ;
36
@@ -13,6 +16,7 @@ use crate::comments::{
1316 has_skip_comment, leading_comments, trailing_comments, Comments , SourceComment ,
1417} ;
1518pub use crate :: context:: PyFormatContext ;
19+ pub use crate :: db:: Db ;
1620pub use crate :: options:: {
1721 DocstringCode , DocstringCodeLineWidth , MagicTrailingComma , PreviewMode , PyFormatOptions ,
1822 QuoteStyle ,
@@ -25,6 +29,7 @@ pub(crate) mod builders;
2529pub mod cli;
2630mod comments;
2731pub ( crate ) mod context;
32+ mod db;
2833pub ( crate ) mod expression;
2934mod generated;
3035pub ( crate ) mod module;
96101 }
97102}
98103
99- #[ derive( Error , Debug ) ]
104+ #[ derive( Error , Debug , salsa :: Update , PartialEq , Eq ) ]
100105pub enum FormatModuleError {
101106 #[ error( transparent) ]
102107 ParseError ( #[ from] ParseError ) ,
@@ -124,6 +129,19 @@ pub fn format_module_ast<'a>(
124129 source : & ' a str ,
125130 options : PyFormatOptions ,
126131) -> FormatResult < Formatted < PyFormatContext < ' a > > > {
132+ format_node ( parsed, comment_ranges, source, options)
133+ }
134+
135+ fn format_node < ' a , N > (
136+ parsed : & ' a Parsed < N > ,
137+ comment_ranges : & ' a CommentRanges ,
138+ source : & ' a str ,
139+ options : PyFormatOptions ,
140+ ) -> FormatResult < Formatted < PyFormatContext < ' a > > >
141+ where
142+ N : AsFormat < PyFormatContext < ' a > > ,
143+ & ' a N : Into < AnyNodeRef < ' a > > ,
144+ {
127145 let source_code = SourceCode :: new ( source) ;
128146 let comments = Comments :: from_ast ( parsed. syntax ( ) , source_code, comment_ranges) ;
129147
@@ -138,6 +156,29 @@ pub fn format_module_ast<'a>(
138156 Ok ( formatted)
139157}
140158
159+ #[ salsa:: tracked( return_ref) ]
160+ pub fn formatted_file ( db : & dyn Db , file : File ) -> Result < FormattedFile , FormatModuleError > {
161+ let options = db. format_options ( file) ;
162+
163+ let parsed = parsed_module ( db, file) ;
164+
165+ if let Some ( first) = parsed. errors ( ) . first ( ) {
166+ return Err ( FormatModuleError :: ParseError ( first. clone ( ) ) ) ;
167+ }
168+
169+ let comment_ranges = CommentRanges :: from ( parsed. tokens ( ) ) ;
170+ let source = source_text ( db, file) ;
171+
172+ let formatted = format_node ( parsed, & comment_ranges, & source, options) ?;
173+ let printed = formatted. print ( ) ?;
174+
175+ if printed. as_code ( ) == & * source {
176+ Ok ( FormattedFile :: Formatted )
177+ } else {
178+ Ok ( FormattedFile :: Reformatted ( printed. into_code ( ) ) )
179+ }
180+ }
181+
141182/// Public function for generating a printable string of the debug comments.
142183pub fn pretty_comments ( module : & Mod , comment_ranges : & CommentRanges , source : & str ) -> String {
143184 let source_code = SourceCode :: new ( source) ;
@@ -146,6 +187,31 @@ pub fn pretty_comments(module: &Mod, comment_ranges: &CommentRanges, source: &st
146187 std:: format!( "{comments:#?}" , comments = comments. debug( source_code) )
147188}
148189
190+ #[ derive( Debug , Clone , Eq , PartialEq , salsa:: Update ) ]
191+ pub enum FormattedFile {
192+ /// The file is already correctly formatted.
193+ Formatted ,
194+
195+ /// The file has been reformatted. The string is its newly formatted content.
196+ Reformatted ( String ) ,
197+ }
198+
199+ impl FormattedFile {
200+ pub fn as_reformatted ( & self ) -> Option < & str > {
201+ match self {
202+ FormattedFile :: Formatted => None ,
203+ FormattedFile :: Reformatted ( code) => Some ( code) ,
204+ }
205+ }
206+
207+ pub fn into_reformatted ( self ) -> Option < String > {
208+ match self {
209+ FormattedFile :: Formatted => None ,
210+ FormattedFile :: Reformatted ( code) => Some ( code) ,
211+ }
212+ }
213+ }
214+
149215#[ cfg( test) ]
150216mod tests {
151217 use std:: path:: Path ;
0 commit comments