use std::rc::Rc;
use ktrs_ast::{Ast, NodeId};
use ktrs_parser::{FileKind, parse_file};
use ktrs_syntax::SyntaxKind::ERROR_ELEMENT;
use super::RuleExecutionContext;
use crate::editorconfig::{EditorConfig, KtlintVersion};
use crate::engine::code::{Code, KtLintException, KtLintParseException};
use crate::engine::ktlint_rule_engine::{KtLintRuleEngine, UTF8_BOM};
use crate::engine::position_in_text_locator::PositionInTextLocator;
use crate::engine::rule_filter::RULE_EXECUTION_RULE_FILTER_LOGGER;
use crate::engine::suppression_locator::SuppressionLocator;
const RULE_EXECUTION_CONTEXT_LOGGER_1_8: &str = "com.pinterest.ktlint.rule.engine.internal.RuleExecutionContext";
pub(crate) fn create_rule_execution_context(
engine: &KtLintRuleEngine,
code: &Code,
) -> Result<RuleExecutionContext, KtLintException> {
let editor_config = engine.editor_config_loader().load(code.file_path.as_deref());
let ktlint_version = editor_config.as_ref().map_or(KtlintVersion::default(), KtlintVersion::of);
let normalized_text = normalize_text(&code.content, ktlint_version);
let position_in_text_locator = PositionInTextLocator::new(&normalized_text);
let psi_file_name = code.psi_file_name();
let parse = parse_file(&normalized_text, FileKind::from_file_name(&psi_file_name));
if let Some(missed) = parse.first_missed_tokens() {
return Err(KtLintException::MissedTokens(missed.clone()));
}
let mut ast = Ast::from_parse(&parse);
ast.set_psi_file_name(&psi_file_name);
if let Some(error_element) = find_error_element(&ast, ast.root()) {
let (line, col) = position_in_text_locator
.locate(ast.utf16_offset(error_element, ast.start_offset(error_element)));
return Err(KtLintParseException {
line,
col,
message: ast.error_description(error_element).to_owned(),
}
.into());
}
let editor_config = editor_config?;
if ktlint_version.is_1_8() {
warn_if_property_is_obsolete(engine, &editor_config, "disabled_rules", "0.49");
warn_if_property_is_obsolete(engine, &editor_config, "ktlint_disabled_rules", "0.49");
}
let setup = engine.rule_setup(editor_config);
for warning in &setup.rule_filter_warnings {
engine.warn(RULE_EXECUTION_RULE_FILTER_LOGGER, || warning.clone());
}
if let Some(message) = &setup.rule_filter_error {
return Err(KtLintException::IllegalState(message.clone()));
}
Ok(RuleExecutionContext {
file_path_or_stdin: if ktlint_version.is_1_8() { code.file_name_or_stdin() } else { code.file_path_or_stdin() },
ast,
suppression_locator: SuppressionLocator::new(&setup.editor_config),
setup,
position_in_text_locator: Rc::new(position_in_text_locator),
})
}
fn normalize_text(text: &str, ktlint_version: KtlintVersion) -> String {
let text = text.replace("\r\n", "\n").replace('\r', "\n");
if ktlint_version.is_1_8() {
return text.replacen(UTF8_BOM, "", 1);
}
match text.strip_prefix(UTF8_BOM) {
Some(rest) => rest.to_owned(),
None => text,
}
}
fn warn_if_property_is_obsolete(engine: &KtLintRuleEngine, editor_config: &EditorConfig, property_name: &str, ktlint_version: &str) {
if editor_config.contains(property_name) {
engine.warn(RULE_EXECUTION_CONTEXT_LOGGER_1_8, || {
format!(
"Editorconfig property '{property_name}' is obsolete and is not used by KtLint starting from version \
{ktlint_version}. Remove the property from all '.editorconfig' files."
)
});
}
}
fn find_error_element(ast: &Ast, node: NodeId) -> Option<NodeId> {
if ast.element_type(node) == ERROR_ELEMENT && !ast.is_leaf_element(node) {
return Some(node);
}
let mut child = ast.first_child_node(node);
while let Some(c) = child {
if let Some(error_element) = find_error_element(ast, c) {
return Some(error_element);
}
child = ast.tree_next(c);
}
None
}