1+ //! Support for future-incompatible warning reporting.
2+
3+ use crate :: core:: { Dependency , PackageId , Workspace } ;
4+ use crate :: sources:: SourceConfigMap ;
5+ use crate :: util:: { iter_join, CargoResult , Config } ;
6+ use anyhow:: { bail, format_err, Context } ;
17use serde:: { Deserialize , Serialize } ;
8+ use std:: collections:: { BTreeSet , HashMap , HashSet } ;
9+ use std:: fmt:: Write as _;
10+ use std:: io:: { Read , Write } ;
11+
12+ pub const REPORT_PREAMBLE : & str = "\
13+ The following warnings were discovered during the build. These warnings are an
14+ indication that the packages contain code that will become an error in a
15+ future release of Rust. These warnings typically cover changes to close
16+ soundness problems, unintended or undocumented behavior, or critical problems
17+ that cannot be fixed in a backwards-compatible fashion, and are not expected
18+ to be in wide use.
19+
20+ Each warning should contain a link for more information on what the warning
21+ means and how to resolve it.
22+ " ;
23+
24+ /// Current version of the on-disk format.
25+ const ON_DISK_VERSION : u32 = 0 ;
226
327/// The future incompatibility report, emitted by the compiler as a JSON message.
428#[ derive( serde:: Deserialize ) ]
529pub struct FutureIncompatReport {
630 pub future_incompat_report : Vec < FutureBreakageItem > ,
731}
832
33+ /// Structure used for collecting reports in-memory.
34+ pub struct FutureIncompatReportPackage {
35+ pub package_id : PackageId ,
36+ pub items : Vec < FutureBreakageItem > ,
37+ }
38+
39+ /// A single future-incompatible warning emitted by rustc.
940#[ derive( Serialize , Deserialize ) ]
1041pub struct FutureBreakageItem {
1142 /// The date at which this lint will become an error.
@@ -24,13 +55,234 @@ pub struct Diagnostic {
2455
2556/// The filename in the top-level `target` directory where we store
2657/// the report
27- pub const FUTURE_INCOMPAT_FILE : & str = ".future-incompat-report.json" ;
58+ const FUTURE_INCOMPAT_FILE : & str = ".future-incompat-report.json" ;
59+ /// Max number of reports to save on disk.
60+ const MAX_REPORTS : usize = 5 ;
2861
62+ /// The structure saved to disk containing the reports.
2963#[ derive( Serialize , Deserialize ) ]
30- pub struct OnDiskReport {
31- // A Cargo-generated id used to detect when a report has been overwritten
32- pub id : String ,
33- // Cannot be a &str, since Serde needs
34- // to be able to un-escape the JSON
35- pub report : String ,
64+ pub struct OnDiskReports {
65+ /// A schema version number, to handle older cargo's from trying to read
66+ /// something that they don't understand.
67+ version : u32 ,
68+ /// The report ID to use for the next report to save.
69+ next_id : u32 ,
70+ /// Available reports.
71+ reports : Vec < OnDiskReport > ,
72+ }
73+
74+ /// A single report for a given compilation session.
75+ #[ derive( Serialize , Deserialize ) ]
76+ struct OnDiskReport {
77+ /// Unique reference to the report for the `--id` CLI flag.
78+ id : u32 ,
79+ /// Report, suitable for printing to the console.
80+ report : String ,
81+ }
82+
83+ impl Default for OnDiskReports {
84+ fn default ( ) -> OnDiskReports {
85+ OnDiskReports {
86+ version : ON_DISK_VERSION ,
87+ next_id : 1 ,
88+ reports : Vec :: new ( ) ,
89+ }
90+ }
91+ }
92+
93+ impl OnDiskReports {
94+ /// Saves a new report.
95+ pub fn save_report (
96+ ws : & Workspace < ' _ > ,
97+ per_package_reports : & [ FutureIncompatReportPackage ] ,
98+ ) -> OnDiskReports {
99+ let mut current_reports = match Self :: load ( ws) {
100+ Ok ( r) => r,
101+ Err ( e) => {
102+ log:: debug!(
103+ "saving future-incompatible reports failed to load current reports: {:?}" ,
104+ e
105+ ) ;
106+ OnDiskReports :: default ( )
107+ }
108+ } ;
109+ let report = OnDiskReport {
110+ id : current_reports. next_id ,
111+ report : render_report ( ws, per_package_reports) ,
112+ } ;
113+ current_reports. next_id += 1 ;
114+ current_reports. reports . push ( report) ;
115+ if current_reports. reports . len ( ) > MAX_REPORTS {
116+ current_reports. reports . remove ( 0 ) ;
117+ }
118+ let on_disk = serde_json:: to_vec ( & current_reports) . unwrap ( ) ;
119+ if let Err ( e) = ws
120+ . target_dir ( )
121+ . open_rw (
122+ FUTURE_INCOMPAT_FILE ,
123+ ws. config ( ) ,
124+ "Future incompatibility report" ,
125+ )
126+ . and_then ( |file| {
127+ let mut file = file. file ( ) ;
128+ file. set_len ( 0 ) ?;
129+ file. write_all ( & on_disk) ?;
130+ Ok ( ( ) )
131+ } )
132+ {
133+ crate :: display_warning_with_error (
134+ "failed to write on-disk future incompatible report" ,
135+ & e,
136+ & mut ws. config ( ) . shell ( ) ,
137+ ) ;
138+ }
139+ current_reports
140+ }
141+
142+ /// Loads the on-disk reports.
143+ pub fn load ( ws : & Workspace < ' _ > ) -> CargoResult < OnDiskReports > {
144+ let report_file = match ws. target_dir ( ) . open_ro (
145+ FUTURE_INCOMPAT_FILE ,
146+ ws. config ( ) ,
147+ "Future incompatible report" ,
148+ ) {
149+ Ok ( r) => r,
150+ Err ( e) => {
151+ if let Some ( io_err) = e. downcast_ref :: < std:: io:: Error > ( ) {
152+ if io_err. kind ( ) == std:: io:: ErrorKind :: NotFound {
153+ bail ! ( "no reports are currently available" ) ;
154+ }
155+ }
156+ return Err ( e) ;
157+ }
158+ } ;
159+
160+ let mut file_contents = String :: new ( ) ;
161+ report_file
162+ . file ( )
163+ . read_to_string ( & mut file_contents)
164+ . with_context ( || "failed to read report" ) ?;
165+ let on_disk_reports: OnDiskReports =
166+ serde_json:: from_str ( & file_contents) . with_context ( || "failed to load report" ) ?;
167+ if on_disk_reports. version != ON_DISK_VERSION {
168+ bail ! ( "unable to read reports; reports were saved from a future version of Cargo" ) ;
169+ }
170+ Ok ( on_disk_reports)
171+ }
172+
173+ /// Returns the most recent report ID.
174+ pub fn last_id ( & self ) -> u32 {
175+ self . reports . last ( ) . map ( |r| r. id ) . unwrap ( )
176+ }
177+
178+ pub fn get_report ( & self , id : u32 , config : & Config ) -> CargoResult < String > {
179+ let report = self . reports . iter ( ) . find ( |r| r. id == id) . ok_or_else ( || {
180+ let available = iter_join ( self . reports . iter ( ) . map ( |r| r. id . to_string ( ) ) , ", " ) ;
181+ format_err ! (
182+ "could not find report with ID {}\n \
183+ Available IDs are: {}",
184+ id,
185+ available
186+ )
187+ } ) ?;
188+ let report = if config. shell ( ) . err_supports_color ( ) {
189+ report. report . clone ( )
190+ } else {
191+ strip_ansi_escapes:: strip ( & report. report )
192+ . map ( |v| String :: from_utf8 ( v) . expect ( "utf8" ) )
193+ . expect ( "strip should never fail" )
194+ } ;
195+ Ok ( report)
196+ }
197+ }
198+
199+ fn render_report (
200+ ws : & Workspace < ' _ > ,
201+ per_package_reports : & [ FutureIncompatReportPackage ] ,
202+ ) -> String {
203+ let mut per_package_reports: Vec < _ > = per_package_reports. iter ( ) . collect ( ) ;
204+ per_package_reports. sort_by_key ( |r| r. package_id ) ;
205+ let mut rendered = String :: new ( ) ;
206+ for per_package in & per_package_reports {
207+ rendered. push_str ( & format ! (
208+ "The package `{}` currently triggers the following future \
209+ incompatibility lints:\n ",
210+ per_package. package_id
211+ ) ) ;
212+ for item in & per_package. items {
213+ rendered. extend (
214+ item. diagnostic
215+ . rendered
216+ . lines ( )
217+ . map ( |l| format ! ( "> {}\n " , l) ) ,
218+ ) ;
219+ }
220+ rendered. push ( '\n' ) ;
221+ }
222+ if let Some ( s) = render_suggestions ( ws, & per_package_reports) {
223+ rendered. push_str ( & s) ;
224+ }
225+ rendered
226+ }
227+
228+ fn render_suggestions (
229+ ws : & Workspace < ' _ > ,
230+ per_package_reports : & [ & FutureIncompatReportPackage ] ,
231+ ) -> Option < String > {
232+ // This in general ignores all errors since this is opportunistic.
233+ let _lock = ws. config ( ) . acquire_package_cache_lock ( ) . ok ( ) ?;
234+ // Create a set of updated registry sources.
235+ let map = SourceConfigMap :: new ( ws. config ( ) ) . ok ( ) ?;
236+ let package_ids: BTreeSet < _ > = per_package_reports
237+ . iter ( )
238+ . map ( |r| r. package_id )
239+ . filter ( |pkg_id| pkg_id. source_id ( ) . is_registry ( ) )
240+ . collect ( ) ;
241+ let source_ids: HashSet < _ > = package_ids
242+ . iter ( )
243+ . map ( |pkg_id| pkg_id. source_id ( ) )
244+ . collect ( ) ;
245+ let mut sources: HashMap < _ , _ > = source_ids
246+ . into_iter ( )
247+ . filter_map ( |sid| {
248+ let source = map. load ( sid, & HashSet :: new ( ) ) . ok ( ) ?;
249+ Some ( ( sid, source) )
250+ } )
251+ . collect ( ) ;
252+ // Query the sources for new versions.
253+ let mut suggestions = String :: new ( ) ;
254+ for pkg_id in package_ids {
255+ let source = match sources. get_mut ( & pkg_id. source_id ( ) ) {
256+ Some ( s) => s,
257+ None => continue ,
258+ } ;
259+ let dep = Dependency :: parse ( pkg_id. name ( ) , None , pkg_id. source_id ( ) ) . ok ( ) ?;
260+ let summaries = source. query_vec ( & dep) . ok ( ) ?;
261+ let versions = itertools:: sorted (
262+ summaries
263+ . iter ( )
264+ . map ( |summary| summary. version ( ) )
265+ . filter ( |version| * version > pkg_id. version ( ) ) ,
266+ ) ;
267+ let versions = versions. map ( |version| version. to_string ( ) ) ;
268+ let versions = iter_join ( versions, ", " ) ;
269+ if !versions. is_empty ( ) {
270+ writeln ! (
271+ suggestions,
272+ "{} has the following newer versions available: {}" ,
273+ pkg_id, versions
274+ )
275+ . unwrap ( ) ;
276+ }
277+ }
278+ if suggestions. is_empty ( ) {
279+ None
280+ } else {
281+ Some ( format ! (
282+ "The following packages appear to have newer versions available.\n \
283+ You may want to consider updating them to a newer version to see if the \
284+ issue has been fixed.\n \n {}",
285+ suggestions
286+ ) )
287+ }
36288}
0 commit comments