1use getset::{Getters, MutGetters};
14use serde_derive::{Serialize, Deserialize};
15
16use std::collections::{HashMap, HashSet};
17use std::{fmt, fmt::Display};
18
19use rpfm_lib::files::{RFile, RFileDecoded};
20use rpfm_lib::utils::*;
21
22use crate::dependencies::Dependencies;
23use crate::diagnostics::*;
24use crate::lua::LuaApi;
25use crate::lua::check::{check_script, LuaDefinitions, LuaIssueKind, LuaKeyReference};
26
27#[derive(Debug, Clone, Default, Getters, MutGetters, Serialize, Deserialize)]
33#[getset(get = "pub", get_mut = "pub")]
34pub struct TextDiagnostic {
35 path: String,
36 pack: String,
37 results: Vec<TextDiagnosticReport>
38}
39
40#[derive(Debug, Clone, Getters, MutGetters, Serialize, Deserialize)]
42#[getset(get = "pub", get_mut = "pub")]
43pub struct TextDiagnosticReport {
44 report_type: TextDiagnosticReportType,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub enum TextDiagnosticReportType {
49 InvalidKey((u64, u64), (u64, u64), String, String, String),
50
51 LuaSyntaxError((u64, u64), (u64, u64), String),
53
54 UnknownMethod((u64, u64), (u64, u64), String, String),
56
57 WrongArgumentCount((u64, u64), (u64, u64), String, usize, Option<usize>, usize),
60
61 UnknownEvent((u64, u64), (u64, u64), String),
63}
64
65impl TextDiagnosticReport {
70 pub fn new(report_type: TextDiagnosticReportType) -> Self {
71 Self {
72 report_type
73 }
74 }
75}
76
77impl DiagnosticReport for TextDiagnosticReport {
78 fn message(&self) -> String {
79 match &self.report_type {
80 TextDiagnosticReportType::InvalidKey(_,_, table, column, key) => "Invalid Key: \"".to_string() + key + "\" is not in table \"" + table + "\", column \"" + column + "\".",
81 TextDiagnosticReportType::LuaSyntaxError(_,_, message) => format!("Syntax Error: {message}"),
82 TextDiagnosticReportType::UnknownMethod(_,_, owner, method) => format!("Unknown Method: \"{method}\" is not documented for \"{owner}\"."),
83 TextDiagnosticReportType::WrongArgumentCount(_,_, function, minimum, maximum, found) => {
84 let expected = match maximum {
85 Some(maximum) if maximum == minimum => minimum.to_string(),
86 Some(maximum) => format!("{minimum} to {maximum}"),
87 None => format!("at least {minimum}"),
88 };
89
90 format!("Wrong Argument Count: \"{function}\" takes {expected} arguments, but {found} were passed.")
91 }
92 TextDiagnosticReportType::UnknownEvent(_,_, event) => format!("Unknown Event: \"{event}\" is not triggered by the game nor by any open script."),
93 }
94 }
95
96 fn level(&self) -> DiagnosticLevel {
97 match self.report_type {
98 TextDiagnosticReportType::InvalidKey(_,_,_,_,_) => DiagnosticLevel::Error,
99 TextDiagnosticReportType::LuaSyntaxError(_,_,_) => DiagnosticLevel::Error,
100 TextDiagnosticReportType::UnknownMethod(_,_,_,_) => DiagnosticLevel::Error,
101 TextDiagnosticReportType::WrongArgumentCount(_,_,_,_,_,_) => DiagnosticLevel::Warning,
102 TextDiagnosticReportType::UnknownEvent(_,_,_) => DiagnosticLevel::Warning,
103 }
104 }
105}
106
107impl TextDiagnosticReportType {
108
109 pub fn range(&self) -> ((u64, u64), (u64, u64)) {
115 match self {
116 Self::InvalidKey(start, end,_,_,_) |
117 Self::LuaSyntaxError(start, end,_) |
118 Self::UnknownMethod(start, end,_,_) |
119 Self::WrongArgumentCount(start, end,_,_,_,_) |
120 Self::UnknownEvent(start, end,_) => (*start, *end),
121 }
122 }
123}
124
125impl Display for TextDiagnosticReportType {
126 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
127 Display::fmt(match self {
128 Self::InvalidKey(_,_,_,_,_) => "InvalidKey",
129 Self::LuaSyntaxError(_,_,_) => "LuaSyntaxError",
130 Self::UnknownMethod(_,_,_,_) => "UnknownMethod",
131 Self::WrongArgumentCount(_,_,_,_,_,_) => "WrongArgumentCount",
132 Self::UnknownEvent(_,_,_) => "UnknownEvent",
133 }, f)
134 }
135}
136
137impl TextDiagnostic {
138 pub fn new(path: &str, pack: &str) -> Self {
139 Self {
140 path: path.to_owned(),
141 pack: pack.to_owned(),
142 results: vec![],
143 }
144 }
145
146 #[allow(clippy::too_many_arguments)]
148 pub fn check(
149 pack_key: &str,
150 file: &RFile,
151 packs: &BTreeMap<String, Pack>,
152 dependencies: &Dependencies,
153 global_ignored_diagnostics: &[String],
154 ignored_fields: &[String],
155 ignored_diagnostics: &HashSet<String>,
156 ignored_diagnostics_for_fields: &HashMap<String, Vec<String>>,
157 lua_api: Option<&LuaApi>,
158 lua_definitions: &LuaDefinitions,
159 ) -> Option<DiagnosticType> {
160
161 if let Ok(RFileDecoded::Text(text)) = file.decoded() {
162 let mut diagnostic = Self::new(file.path_in_container_raw(), pack_key);
163
164 let text = text.contents();
165 let mut start_pos = 0;
166
167 while let Some(pos) = text[start_pos..].find("--@db ") {
169 if let Some(end_line) = text[start_pos + pos..].find('\n') {
170
171 let table_data = text[start_pos + pos + 6..start_pos + pos + end_line].split(' ').collect::<Vec<_>>();
173
174 if table_data.len() >= 2 {
176 let table_name = if table_data[0].ends_with("_tables") { table_data[0].to_owned() } else { table_data[0].to_owned() + "_tables" };
177 let table_column = if table_data[1].ends_with("\r") {
178 &table_data[1][..table_data[1].len() - 1]
179 } else {
180 table_data[1]
181 };
182
183 let index_to_check = if let Some(indexes) = table_data.get(2) {
184 indexes.split(",")
185 .filter_map(|x| x.parse::<usize>().ok())
186 .collect()
187 } else {
188 vec![]
189 };
190
191 let (next_line_start, next_line_end) = match text[start_pos + pos + 6..].find('\n') {
193 Some(nls) => if text.as_bytes().get(start_pos + pos + 6 + nls + 1).is_some() {
194 match text[start_pos + pos + 6 + nls + 1..].find('\n') {
195 Some(nle) => (start_pos + pos + 6 + nls + 1, start_pos + pos + 6 + nls + 1 + nle),
196 None => break,
197 }
198 } else {
199 break;
200 }
201
202 None => break,
203 };
204
205 let (keys, data_start, data_end) = {
229 let mut vals = (vec![], 0, 0);
230
231 if let Some(data_start) = text[next_line_start..next_line_end].find('{') {
232 if let Some(data_end) = text[next_line_start + data_start..].find('}') {
233
234 let data_to_search = &text[next_line_start + data_start + 1..next_line_start + data_start + data_end];
236
237 if data_to_search.contains('\n') || data_to_search.contains('\r') {
239
240 if data_to_search.contains('=') {
242 let data_split = data_to_search.split('\n')
243 .filter_map(|x| {
244 let spl = x.split('=')
245 .map(|y| y.split('\"').collect::<Vec<_>>());
246
247 let mut keys = vec![];
248 for (i, data) in spl.enumerate() {
249 if index_to_check.contains(&i) && data.len() == 3 {
250 keys.push(data[1].to_owned());
251 }
252 }
253
254 if !keys.is_empty() {
255 Some(keys)
256 } else {
257 None
258 }
259 })
260 .flatten()
261 .collect::<Vec<_>>();
262
263 vals = (data_split, data_start, data_end)
264 }
265
266 else {
268 let data_split = data_to_search.split('\n')
269 .filter_map(|x| {
270
271 let spl = x.split('\"').collect::<Vec<_>>();
273 if spl.len() == 3 {
274 Some(spl[1].to_owned())
275 } else {
276 None
277 }
278 })
279 .collect::<Vec<_>>();
280
281 vals = (data_split, data_start, data_end)
282 }
283 }
284
285 else if data_to_search.contains('=') {
287 let data_split = data_to_search.split(',')
288 .filter_map(|x| {
289 let spl = x.split('=')
290 .map(|y| y.split('\"').collect::<Vec<_>>());
291
292 let mut keys = vec![];
293 for (i, data) in spl.enumerate() {
294 if index_to_check.contains(&i) && data.len() == 3 {
295 keys.push(data[1].to_owned());
296 }
297 }
298
299 if !keys.is_empty() {
300 Some(keys)
301 } else {
302 None
303 }
304 })
305 .flatten()
306 .collect::<Vec<_>>();
307
308 vals = (data_split, data_start, data_end)
309 }
310
311 else {
313 let data_split = data_to_search.split(',')
314 .filter_map(|x| {
315
316 let spl = x.split('\"').collect::<Vec<_>>();
318 if spl.len() == 3 {
319 Some(spl[1].to_owned())
320 } else {
321 None
322 }
323 })
324 .collect::<Vec<_>>();
325
326 vals = (data_split, data_start, data_end)
327 }
328 }
329 }
330
331 else if let Some(data_start) = text[next_line_start..next_line_end].find('\"') {
333 if text.as_bytes().get(next_line_start + data_start + 1).is_some() {
335 if let Some(data_end) = text[next_line_start + data_start + 1..].find('\"') {
336 if text.as_bytes().get(next_line_start + data_start + 1 + data_end).is_some() {
337 let data_to_search = &text[next_line_start + data_start + 1..next_line_start + data_start + 1 + data_end];
338 vals = (vec![data_to_search.to_string()], data_start, data_end)
339 }
340 }
341 }
342 }
343
344 vals
345 };
346
347 let mut not_found = HashMap::new();
348
349 if let Ok(mut tables) = dependencies.db_data(&table_name, true, true) {
351 for pack in packs.values() {
352 tables.append(&mut pack.files_by_path(&ContainerPath::Folder("db/".to_string() + &table_name + "/"), true));
353 }
354 tables.reverse();
355
356 if tables.is_empty() {
358 start_pos = next_line_start + data_start + data_end;
359 continue;
360 }
361
362 for key in &keys {
363 let key_to_check = key.trim();
364
365 let start_cursor = line_column_from_string_pos(text, (next_line_start + data_start + 1) as u64);
367 let end_cursor = line_column_from_string_pos(text, (next_line_start + data_start + 1 + data_end) as u64);
368
369 let mut found = false;
370 for table in &tables {
371 if let Ok(RFileDecoded::DB(table)) = table.decoded() {
372 let definition = table.definition();
373 if let Some(column) = definition.column_position_by_name(table_column) {
374 for row in table.data().iter() {
375 if row[column].data_to_string() == *key_to_check {
376 found = true;
377 break;
378 }
379 }
380
381 if found {
382 break;
383 }
384 }
385 }
386 }
387
388 if !found {
389 not_found.insert(key_to_check, (start_cursor, end_cursor));
390 }
391 }
392
393 for (key, (start, end)) in ¬_found {
394 if !Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some("InvalidKey"), ignored_fields, ignored_diagnostics, ignored_diagnostics_for_fields) {
395 let result = TextDiagnosticReport::new(TextDiagnosticReportType::InvalidKey(*start, *end, table_name.to_string(), table_column.to_string(), key.to_string()));
396 diagnostic.results_mut().push(result);
397 }
398 }
399 }
400
401 start_pos = next_line_start + data_start + data_end;
402 }
403 }
404 }
405
406 let is_ignored = |report_type: &TextDiagnosticReportType| Diagnostics::ignore_diagnostic(global_ignored_diagnostics, None, Some(&report_type.to_string()), ignored_fields, ignored_diagnostics, ignored_diagnostics_for_fields);
407 let result = check_script(text, lua_api, lua_definitions);
408 for issue in result.issues() {
409 let (start, end) = *issue.range();
410 let report_type = match issue.kind().clone() {
411 LuaIssueKind::SyntaxError(message) => TextDiagnosticReportType::LuaSyntaxError(start, end, message),
412 LuaIssueKind::UnknownMethod(owner, method) => TextDiagnosticReportType::UnknownMethod(start, end, owner, method),
413 LuaIssueKind::WrongArgumentCount(function, minimum, maximum, found) => TextDiagnosticReportType::WrongArgumentCount(start, end, function, minimum, maximum, found),
414 LuaIssueKind::UnknownEvent(event) => TextDiagnosticReportType::UnknownEvent(start, end, event),
415 };
416
417 if !is_ignored(&report_type) {
418 diagnostic.results_mut().push(TextDiagnosticReport::new(report_type));
419 }
420 }
421
422 for report_type in Self::invalid_keys(result.key_references(), packs, dependencies) {
423 if !is_ignored(&report_type) {
424 diagnostic.results_mut().push(TextDiagnosticReport::new(report_type));
425 }
426 }
427
428 if !diagnostic.results().is_empty() {
429 Some(DiagnosticType::Text(diagnostic))
430 } else { None }
431 } else { None }
432 }
433
434 fn invalid_keys(key_references: &[LuaKeyReference], packs: &BTreeMap<String, Pack>, dependencies: &Dependencies) -> Vec<TextDiagnosticReportType> {
448 let mut keys_by_table: HashMap<&str, Option<(String, HashSet<String>)>> = HashMap::new();
449 let mut reports = vec![];
450
451 for reference in key_references {
452 let table_keys = keys_by_table.entry(reference.table())
453 .or_insert_with(|| dependencies.db_key_values(Some(packs), reference.table()));
454
455 if let Some((column, keys)) = table_keys {
456 if !keys.contains(reference.key()) {
457 let (start, end) = *reference.range();
458 reports.push(TextDiagnosticReportType::InvalidKey(start, end, reference.table().to_owned(), column.to_owned(), reference.key().to_owned()));
459 }
460 }
461 }
462
463 reports
464 }
465}