mod context;
mod new_line;
mod string_template_indenter;
mod visit_declarations;
mod visit_expressions;
use std::sync::LazyLock;
use ktrs_ast::{Ast, NodeId};
use ktrs_editorconfig::PropertyType;
use ktrs_editorconfig::property_type::boolean_value_parser;
use ktrs_syntax::SyntaxKind::{
self, ANNOTATED_EXPRESSION, ARRAY_ACCESS_EXPRESSION, BINARY_EXPRESSION, BINARY_WITH_TYPE, CALL_EXPRESSION, CLASS,
CLOSING_QUOTE, CONDITION, CONTEXT_PARAMETER_LIST, DELEGATED_SUPER_TYPE_ENTRY, DESTRUCTURING_DECLARATION,
DOT_QUALIFIED_EXPRESSION, FOR, FUN, FUNCTION_LITERAL, IDENTIFIER, IF, IS_EXPRESSION, LBRACE, LBRACKET,
LITERAL_STRING_TEMPLATE_ENTRY, LONG_STRING_TEMPLATE_ENTRY, LPAR, NULLABLE_TYPE, OBJECT_DECLARATION, PARENTHESIZED,
POSTFIX_EXPRESSION, PREFIX_EXPRESSION, PRIMARY_CONSTRUCTOR, PROPERTY, PROPERTY_ACCESSOR, SAFE_ACCESS_EXPRESSION,
SECONDARY_CONSTRUCTOR, STRING_TEMPLATE, SUPER_TYPE_LIST, TRY, TYPE_ARGUMENT_LIST, TYPE_CONSTRAINT_LIST,
TYPE_PARAMETER_LIST, TYPE_REFERENCE, USER_TYPE, VALUE_ARGUMENT, VALUE_ARGUMENT_LIST, VALUE_PARAMETER,
VALUE_PARAMETER_LIST, WHEN, WHEN_ENTRY, WHERE_KEYWORD, WHILE,
};
use crate::ast_node_edit::AstNodeEdit;
use crate::ast_node_extension::AstNodeExtension;
use crate::editorconfig::{
CODE_STYLE_PROPERTY, CodeStyleValue, EditorConfigProperty, INDENT_SIZE_PROPERTY, INDENT_STYLE_PROPERTY, PropertyRef,
};
use crate::element_type::{KDOC, TYPEALIAS};
use crate::indent_config::IndentConfig;
use crate::rule::{About, EditorConfig, Emit, RuleId, RuleV2, TraversalState};
use crate::rules::STANDARD_RULE_ABOUT;
use string_template_indenter::StringTemplateIndenter;
const KDOC_CONTINUATION_INDENT: &str = " ";
const TYPE_CONSTRAINT_CONTINUATION_INDENT: &str = " "; const CHAINABLE_EXPRESSION: [SyntaxKind; 2] = [DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION];
static INDENT_WHEN_ARROW_ON_NEW_LINE_TYPE: PropertyType<bool> = PropertyType {
name: "ij_kotlin_indent_before_arrow_on_new_line",
description: "Indent the arrow in a when-entry if the arrow starts on a new line.",
parser: boolean_value_parser,
possible_values: &["true", "false"],
lower_casing: true,
};
pub static INDENT_WHEN_ARROW_ON_NEW_LINE: LazyLock<EditorConfigProperty<bool>> =
LazyLock::new(|| EditorConfigProperty::new(&INDENT_WHEN_ARROW_ON_NEW_LINE_TYPE, false));
pub struct IndentationRule {
code_style: CodeStyleValue,
indent_config: IndentConfig,
indent_when_arrow_on_new_line: bool,
indent_context_stack: Vec<IndentContext>,
string_template_indenter: Option<StringTemplateIndenter>,
traversal: TraversalState,
}
impl IndentationRule {
pub fn new() -> IndentationRule {
IndentationRule {
code_style: CODE_STYLE_PROPERTY.default_value,
indent_config: IndentConfig::default_indent_config(),
indent_when_arrow_on_new_line: INDENT_WHEN_ARROW_ON_NEW_LINE.default_value,
indent_context_stack: Vec::new(),
string_template_indenter: None,
traversal: TraversalState::default(),
}
}
fn is_official(&self) -> bool {
self.code_style == CodeStyleValue::KtlintOfficial
}
}
impl Default for IndentationRule {
fn default() -> Self {
Self::new()
}
}
impl RuleV2 for IndentationRule {
fn rule_id(&self) -> RuleId {
RuleId("standard:indent")
}
fn about(&self) -> About {
STANDARD_RULE_ABOUT
}
fn uses_editor_config_properties(&self) -> Vec<PropertyRef> {
vec![
PropertyRef::from(&*CODE_STYLE_PROPERTY),
PropertyRef::from(&*INDENT_SIZE_PROPERTY),
PropertyRef::from(&*INDENT_STYLE_PROPERTY),
PropertyRef::from(&*INDENT_WHEN_ARROW_ON_NEW_LINE),
]
}
fn traversal_state(&self) -> Option<TraversalState> {
Some(self.traversal)
}
fn before_first_node(&mut self, editor_config: &EditorConfig) {
self.code_style = editor_config.get(&CODE_STYLE_PROPERTY);
self.indent_config = IndentConfig::new(editor_config.get(&INDENT_STYLE_PROPERTY), editor_config.get(&INDENT_SIZE_PROPERTY));
if self.indent_config.disabled() {
self.traversal.stop_traversal_of_ast();
}
self.indent_when_arrow_on_new_line = editor_config.get(&INDENT_WHEN_ARROW_ON_NEW_LINE);
}
fn before_visit_child_nodes(&mut self, ast: &mut Ast, node: NodeId, emit: &mut Emit<'_>) {
if ast.is_root(node) {
if let Some(whitespace_without_newline) = ast.next_leaf(node).filter(|&it| ast.is_white_space_without_newline(it)) {
emit(ast, ast.start_offset(node), "Unexpected indentation", true).if_autocorrect_allowed(|| ast.remove(whitespace_without_newline));
}
let no_indent_zone = start_no_indent_zone(ast, node);
self.indent_context_stack.push(no_indent_zone);
}
let element_type = ast.element_type(node);
let parent_type = || ast.parent(node).map(|it| ast.element_type(it));
let next_code_sibling_type = || ast.next_code_sibling(node).map(|it| ast.element_type(it));
let ast_ref: &Ast = ast;
if ast.is_white_space_with_newline(node) {
if let Some(last) = self.indent_context_stack.last_mut().filter(|it| !it.activated) {
last.activated = true;
}
self.visit_new_line_indentation(ast, node, emit);
} else if matches!(element_type, CONTEXT_PARAMETER_LIST | LONG_STRING_TEMPLATE_ENTRY | STRING_TEMPLATE | VALUE_ARGUMENT_LIST) {
self.start_indent_context(ast_ref, node, Args::default().last_child_indent(""));
} else if (element_type == SUPER_TYPE_LIST && !is_preceded_by_comment(ast_ref, node))
|| (ast.is_part_of_comment(node) && next_code_sibling_type() == Some(SUPER_TYPE_LIST))
{
if self.is_official() {
let super_type_list =
if ast.is_part_of_comment(node) { ast.next_code_leaf(node).expect("NullPointerException: nextCodeLeaf!!") } else { node };
if is_part_of_class_with_a_multiline_primary_constructor(ast_ref, super_type_list) {
self.start_indent_context(ast_ref, node, Args::default().activated());
}
}
} else if element_type == VALUE_ARGUMENT {
self.visit_value_argument(ast_ref, node);
} else if element_type == SECONDARY_CONSTRUCTOR {
self.visit_secondary_constructor(ast_ref, node);
} else if element_type == PARENTHESIZED {
if self.is_official() {
self.start_indent_context(ast_ref, node, Args::default().last_child_indent(""));
} else if ast.parent(node).and_then(|it| ast.parent(it)).map(|it| ast.element_type(it)) != Some(IF) {
self.start_indent_context(ast_ref, node, Args::default());
}
} else if matches!(element_type, TYPE_ARGUMENT_LIST | TYPE_PARAMETER_LIST) {
if self.is_official() {
self.start_indent_context(ast_ref, node, Args::default().last_child_indent(""));
} else {
self.start_indent_context(ast_ref, node, Args::default());
}
} else if matches!(element_type, BINARY_WITH_TYPE | USER_TYPE) {
self.start_indent_context(ast_ref, node, Args::default());
} else if matches!(element_type, IS_EXPRESSION | PREFIX_EXPRESSION | POSTFIX_EXPRESSION) {
self.start_indent_context(ast_ref, node, Args::default());
} else if matches!(element_type, DELEGATED_SUPER_TYPE_ENTRY | ANNOTATED_EXPRESSION | TYPE_REFERENCE) {
self.start_indent_context(ast_ref, node, Args::default().child_indent(""));
} else if element_type == IF {
self.visit_if(ast_ref, node);
} else if element_type == LBRACE {
self.visit_lbrace(ast_ref, node);
} else if element_type == VALUE_PARAMETER_LIST && parent_type() != Some(FUNCTION_LITERAL) {
self.start_indent_context(ast_ref, node, Args::default().last_child_indent(""));
} else if element_type == LPAR && next_code_sibling_type() == Some(CONDITION) {
self.visit_lpar_before_condition(ast_ref, node);
} else if element_type == VALUE_PARAMETER {
self.visit_value_parameter(ast_ref, node);
} else if element_type == FUN {
self.visit_fun(ast_ref, node);
} else if element_type == CLASS {
self.visit_class(ast_ref, node);
} else if element_type == OBJECT_DECLARATION {
self.visit_object_declaration(ast_ref, node);
} else if element_type == BINARY_EXPRESSION {
self.visit_binary_expression(ast_ref, node);
} else if CHAINABLE_EXPRESSION.contains(&element_type) {
if self.is_official()
&& element_type == DOT_QUALIFIED_EXPRESSION
&& parent_type() == Some(ARRAY_ACCESS_EXPRESSION)
&& ast.parent(node).and_then(|it| ast.parent(it)).map(|it| ast.element_type(it)) == Some(CALL_EXPRESSION)
{
let parent = ast.parent(node).expect("NullPointerException: parent!!");
let grand_parent = ast.parent(parent).expect("NullPointerException: parent!!.parent!!");
self.start_indent_context(ast_ref, parent, Args::default().to(ast.last_child_leaf_or_self(grand_parent)));
} else if is_elvis_operator(ast_ref, ast.prev_code_sibling(node)) {
self.start_indent_context(ast_ref, node, Args::default());
} else if parent_type().is_some_and(|it| CHAINABLE_EXPRESSION.contains(&it)) {
} else {
self.start_indent_context(ast_ref, node, Args::default());
}
} else if element_type == IDENTIFIER && parent_type() == Some(PROPERTY) {
self.visit_identifier_in_property(ast_ref, node);
} else if element_type == LITERAL_STRING_TEMPLATE_ENTRY && next_code_sibling_type() == Some(CLOSING_QUOTE) {
self.visit_white_space_before_closing_quote(ast, node, emit);
} else if element_type == WHEN {
self.visit_when(ast_ref, node);
} else if element_type == WHEN_ENTRY {
self.visit_when_entry(ast_ref, node);
} else if element_type == WHERE_KEYWORD && next_code_sibling_type() == Some(TYPE_CONSTRAINT_LIST) {
self.visit_where_keyword_before_type_constraint_list(ast_ref, node);
} else if element_type == KDOC {
self.visit_kdoc(ast_ref, node);
} else if matches!(element_type, PROPERTY_ACCESSOR | TYPEALIAS) {
self.visit_property_accessor(ast_ref, node);
} else if matches!(element_type, FOR | WHILE) {
self.visit_conditional_loop(ast_ref, node);
} else if element_type == LBRACKET {
self.visit_l_bracket(ast_ref, node);
} else if element_type == NULLABLE_TYPE {
self.visit_nullable_type(ast_ref, node);
} else if element_type == DESTRUCTURING_DECLARATION {
self.visit_destructuring_declaration(ast_ref, node);
} else if element_type == TRY {
self.visit_try_catch_finally(ast_ref, node);
}
}
fn after_visit_child_nodes(&mut self, ast: &mut Ast, node: NodeId, _emit: &mut Emit<'_>) {
self.after_visit_child_nodes_impl(ast, node);
}
fn visits_after_child_nodes(&self) -> bool {
true
}
fn after_last_node(&mut self) {
self.after_last_node_impl();
}
}
fn is_part_of_class_with_a_multiline_primary_constructor(ast: &Ast, node: NodeId) -> bool {
ast.parent(node)
.filter(|&it| ast.element_type(it) == CLASS)
.and_then(|it| ast.find_child_by_type(it, PRIMARY_CONSTRUCTOR))
.is_some_and(|it| ast.text_contains(it, '\n'))
}
#[derive(Clone, Debug)]
struct IndentContext {
from_ast_node: NodeId,
to_ast_node: NodeId,
node_indent: String,
first_child_indent: String,
child_indent: String,
last_child_indent: String,
activated: bool,
node_types: [(SyntaxKind, bool); 2],
}
impl std::fmt::Display for IndentContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let [from, to] = self.node_types.map(|(kind, is_leaf)| ktrs_ast::psi::element_to_string(kind, is_leaf));
write!(
f,
"IndentContext(fromASTNode={from}, toASTNode={to}, nodeIndent={}, firstChildIndent={}, childIndent={}, \
lastChildIndent={}, activated={})",
self.node_indent, self.first_child_indent, self.child_indent, self.last_child_indent, self.activated
)
}
}
impl IndentContext {
fn node_types(ast: &Ast, from_ast_node: NodeId, to_ast_node: NodeId) -> [(SyntaxKind, bool); 2] {
[from_ast_node, to_ast_node].map(|it| (ast.element_type(it), ast.is_leaf_element(it)))
}
fn indent(&self) -> String {
if self.activated { format!("{}{}", self.node_indent, self.child_indent) } else { self.node_indent.clone() }
}
}
#[derive(Clone, Copy)]
struct StartedIndentContext {
from_ast_node: NodeId,
}
impl StartedIndentContext {
fn prev_code_leaf(self, ast: &Ast) -> NodeId {
ast.prev_code_leaf(self.from_ast_node).expect("NullPointerException: fromASTNode.prevCodeLeaf!!")
}
}
#[derive(Default)]
struct Args {
to_ast_node: Option<NodeId>,
node_indent: Option<String>,
child_indent: Option<String>,
first_child_indent: Option<String>,
last_child_indent: Option<String>,
activated: bool,
}
impl Args {
fn to(mut self, to_ast_node: NodeId) -> Args {
self.to_ast_node = Some(to_ast_node);
self
}
fn node_indent(mut self, node_indent: impl Into<String>) -> Args {
self.node_indent = Some(node_indent.into());
self
}
fn child_indent(mut self, child_indent: impl Into<String>) -> Args {
self.child_indent = Some(child_indent.into());
self
}
fn first_child_indent(mut self, first_child_indent: impl Into<String>) -> Args {
self.first_child_indent = Some(first_child_indent.into());
self
}
fn last_child_indent(mut self, last_child_indent: impl Into<String>) -> Args {
self.last_child_indent = Some(last_child_indent.into());
self
}
fn activated(mut self) -> Args {
self.activated = true;
self
}
}
use new_line::{is_elvis_operator, is_preceded_by_comment, start_no_indent_zone};