From 138a82ca076a8ab45863da4f4eaece1d418c7652 Mon Sep 17 00:00:00 2001 From: Joshua Gardner Date: Mon, 18 May 2026 10:36:58 +1000 Subject: [PATCH] Add support for `not in` and `is not` compound operators --- CHANGELOG.md | 1 + core/CHANGELOG.md | 1 + .../generators/optimising_line_formatter.rs | 84 +++++++++++++++++++ .../optimising_line_formatter/contexts.rs | 72 ++++++++++++++++ .../rules/optimising_line_formatter/mod.rs | 26 +++++- .../optimising_line_formatter/requirements.rs | 3 + 6 files changed, 184 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a4638fa..2bef3e93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added support for Delphi 13 `noreturn` directive. - Added support for Delphi 13 `unmanaged` generic constraint. - Added support for `if else` ternary expressions. +- Added support for `not in` and `is not` compound operators. ## [0.7.0] - 2025-11-11 diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 7b75842e..c30deed1 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `KeywordKind::is_generic_constraint`. - Added support for Delphi 13 `unmanaged` generic constraint. - Added support for `if else` ternary expressions. +- Added support for `not in` and `is not` compound operators. ## 0.7.0 - 2025-11-11 diff --git a/core/datatests/generators/optimising_line_formatter.rs b/core/datatests/generators/optimising_line_formatter.rs index 67335c0a..f2abad3f 100644 --- a/core/datatests/generators/optimising_line_formatter.rs +++ b/core/datatests/generators/optimising_line_formatter.rs @@ -361,6 +361,7 @@ mod comments { child_lines::generate(root_dir); conditional_directives::generate(root_dir); individual_block::generate(root_dir); + compound_operators::generate(root_dir); } mod midline_line { @@ -705,6 +706,44 @@ mod comments { ); } } + + mod compound_operators { + use super::*; + + pub fn generate(root_dir: &Path) { + generate_test_cases!( + root_dir, + not_in = " + AA := AAA {} not {} in {} BBB; + AAA := + AAA {} not {} in {} BBB; + AAA := + AAAAA {} + not {} in {} BBBB; + AAA := + AAAAA {} + not {} in {} BBBBBBBBB; + AAA := + AAAAA + {} + not + {} in + {} BBBBBBBBB; + AAA := + AAAAA + { + } + not + { + } + in + { + } + BBBBBBBBB; + ", + ); + } + } } mod anonymous { @@ -4344,6 +4383,51 @@ mod expressions { and DDDDDDD; ", + compound = " + A := AAAAAAAA not in BBBBBBBB; + A := + AAAAAAAA not in BBBBBBBBB; + A := + AAAAAAAAA + not in BBBBBBBBB; + A := + AAAAAAAAA + not in BBBBBBBBBBBBBBB; + A := + AAAAAA + BBBBBB + CCCCCCCC + not in DDDDDDDDDDDDDD; + A := + AAAAAA + BBBBBB + CCCCCCCC + not in DDDDD + EEEEEE; + A := + AAAAAAA + + BBBBBBB + + CCCCCCC + not in DDDDDDDDDDDDDD; + A := + AAAAAA + BBBBBB + CCCCCCCC + not in DDDDDD + + EEEEEE; + A := AAAAAAAA is not BBBBBBBB; + A := + AAAAAAAA is not BBBBBBBBB; + A := + AAAAAAAAA + is not BBBBBBBBB; + A := + AAAAAAAAA + is not BBBBBBBBBBBBBBB; + A := (AAA not in [DDD + EEE]); + A := + (AAAA not in [DDD + EEE]); + A := + (AAAAA + not in [DDD + EEE]); + A := + (AAAAA + not in [ + DDDDD + EEEE]); + ", ); } } diff --git a/core/src/rules/optimising_line_formatter/contexts.rs b/core/src/rules/optimising_line_formatter/contexts.rs index d0261ef3..7854bc98 100644 --- a/core/src/rules/optimising_line_formatter/contexts.rs +++ b/core/src/rules/optimising_line_formatter/contexts.rs @@ -420,6 +420,15 @@ impl<'a> SpecificContextStack<'a> { ) .cloned() } + fn get_next_real_token_type_from_line_index(&self, line_index: u32) -> Option { + self.formatting_contexts + .line + .get_tokens() + .iter() + .skip(line_index as usize + 1) + .map(|index| self.formatting_contexts.token_types[*index]) + .find(|token_type| !token_type.is_comment_or_compiler_directive()) + } /// Updates all contexts to reflect the decision provided. pub(super) fn update_contexts(&self, node: &mut FormattingNode, decision: RawDecision) { @@ -662,6 +671,18 @@ impl<'a> SpecificContextStack<'a> { _ => {} } } + (Some(op1), Some(op2)) if (op1, op2).get_operator_precedence().is_some() => { + // In the middle of a compound operator, do nothing + } + (_, Some(op @ (TT::Op(_) | TT::Keyword(_)))) + if self + .get_next_real_token_type_from_line_index(line_index) + .is_some_and(|token_type| { + (op, token_type).get_operator_precedence().is_some() + }) => + { + self.update_operator_precedences(node, is_break); + } (prev, Some(op @ (TT::Op(_) | TT::Keyword(_)))) if op.get_operator_precedence().is_some() && is_binary(op, prev.as_ref()) => { @@ -852,6 +873,15 @@ impl<'a> LineFormattingContexts<'a> { .map(|id| token_types[*id]) .collect::>(); let mut current = next_token_types.pop(); + + fn next_real_token_type(token_types: &[TokenType]) -> Option { + token_types + .iter() + .rev() + .find(|token_type| !token_type.is_comment_or_compiler_directive()) + .cloned() + } + while let Some(current_token_type) = current { if !current_token_type.is_comment_or_compiler_directive() { let last_context_type = contexts.current_context.get().context_type; @@ -982,6 +1012,21 @@ impl<'a> LineFormattingContexts<'a> { TT::ConditionalDirective(kind) if kind.is_else() => { contexts.push_operators(); } + op if (op, ¤t_token_type) + .get_operator_precedence() + .is_some() => + { + // In the middle of a compound operator, do nothing + } + op if prev_token_types + .iter() + .rev() + .nth(1) + .and_then(|prev| (prev, op).get_operator_precedence()) + .is_some() => + { + contexts.push_operators(); + } op if op.get_operator_precedence().is_some() && is_binary(*op, last_semantic_token_type!(1)) => { @@ -1197,6 +1242,22 @@ impl<'a> LineFormattingContexts<'a> { } } + op if prev_token_types + .last() + .and_then(|prev| (prev, &op).get_operator_precedence()) + .is_some() => + { + // We are in the middle of a compound operator, do nothing + } + op if next_real_token_type(&next_token_types) + .and_then(|next| (op, next).get_operator_precedence()) + .is_some() => + { + let op_prec = next_real_token_type(&next_token_types) + .and_then(|next| (op, next).get_operator_precedence()) + .unwrap(); + contexts.pop_until_and_retain(CT::Precedence(op_prec)); + } op if op.get_operator_precedence().is_some() && is_binary(op, last_semantic_token_type!()) => { @@ -2260,6 +2321,17 @@ mod tests { 1 Precedence(3) ^----------- 1 Precedence(2) ^-----$ "}, + not_in_operator = {" + AA + BB not in CC + 1 Base ^---------------- + 1 Precedence(4) ^---------------- + 1 Precedence(3) ^-----$ + "}, + is_not_operator = {" + AA is not BB + 1 Base ^----------- + 1 Precedence(4) ^----------- + "}, routine_arguments = {" AA(BB, CC) + DD 1 Base ^-------------- diff --git a/core/src/rules/optimising_line_formatter/mod.rs b/core/src/rules/optimising_line_formatter/mod.rs index 469751ae..26b3a062 100644 --- a/core/src/rules/optimising_line_formatter/mod.rs +++ b/core/src/rules/optimising_line_formatter/mod.rs @@ -1259,11 +1259,11 @@ impl<'this> InternalOptimisingLineFormatter<'this, '_> { const HIGHEST_PRECEDENCE: u8 = 0; const LOWEST_PRECEDENCE: u8 = 5; trait OperatorPrecedence { - fn get_operator_precedence(self) -> Option; + fn get_operator_precedence(&self) -> Option; } -impl OperatorPrecedence for TokenType { - fn get_operator_precedence(self) -> Option { +impl OperatorPrecedence for &TokenType { + fn get_operator_precedence(&self) -> Option { match self { TT::Op(OK::Dot) => Some(0), @@ -1301,6 +1301,26 @@ impl OperatorPrecedence for TokenType { } } } +impl OperatorPrecedence for TokenType { + fn get_operator_precedence(&self) -> Option { + (&self).get_operator_precedence() + } +} +impl OperatorPrecedence for (&TokenType, &TokenType) { + fn get_operator_precedence(&self) -> Option { + match self { + // Compound operators `not in` and `is not` + (TT::Keyword(KK::Not), op @ TT::Keyword(KK::In(InKind::Op))) + | (op @ TT::Keyword(KK::Is), TT::Keyword(KK::Not)) => op.get_operator_precedence(), + _ => None, + } + } +} +impl OperatorPrecedence for (TokenType, TokenType) { + fn get_operator_precedence(&self) -> Option { + (&self.0, &self.1).get_operator_precedence() + } +} /// When dealing with precedences, binary and unary operators should often be /// treated differently. diff --git a/core/src/rules/optimising_line_formatter/requirements.rs b/core/src/rules/optimising_line_formatter/requirements.rs index bee6c38c..80622536 100644 --- a/core/src/rules/optimising_line_formatter/requirements.rs +++ b/core/src/rules/optimising_line_formatter/requirements.rs @@ -213,6 +213,9 @@ impl InternalOptimisingLineFormatter<'_, '_> { .get_last_context(CT::RaiseAt) .map(|(_, data)| data.is_broken | data.is_child_broken) .if_else_or_default(DR::MustBreak, DR::Indifferent), + (Some(op1), Some(op2)) if (op1, op2).get_operator_precedence().is_some() => { + DR::MustNotBreak + } (prev, Some(op @ (TT::Op(_) | TT::Keyword(_)))) if op.get_operator_precedence().is_some() && is_binary(op, prev.as_ref()) => {