cgrep-9.0.0: src/CGrep/Semantic/Tests.hs
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
module CGrep.Semantic.Tests (
filterTests,
) where
import CGrep.FileType (FileType (..))
import CGrep.Parser.Token (Token, isTokenOperator, tToken, isTokenBracket, isTokenIdentifier, isTokenKeyword)
import qualified Data.Text as T
class LanguageTestFilter (lang :: FileType) where
langFilter :: Maybe Bool -> [Token] -> [Token]
-- | Rust-specific instance using the helper functions.
instance LanguageTestFilter 'Rust where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideRust keepTests tokens
instance LanguageTestFilter 'Go where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideGo keepTests tokens
instance LanguageTestFilter 'Java where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideJava keepTests tokens
instance LanguageTestFilter 'Kotlin where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideJava keepTests tokens
instance LanguageTestFilter 'C where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideC keepTests tokens
instance LanguageTestFilter 'Cpp where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideC keepTests tokens
instance LanguageTestFilter 'Python where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsidePython keepTests tokens
instance LanguageTestFilter 'Zig where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideZig keepTests tokens
instance LanguageTestFilter 'Javascript where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideJavascript keepTests tokens
instance LanguageTestFilter 'Scala where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideScala keepTests tokens
instance LanguageTestFilter 'Haskell where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideHaskell keepTests tokens
instance LanguageTestFilter 'Csharp where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideCsharp keepTests tokens
instance LanguageTestFilter 'Fsharp where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideFsharp keepTests tokens
instance LanguageTestFilter 'Dart where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideDart keepTests tokens
instance LanguageTestFilter 'Elixir where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideElixir keepTests tokens
instance LanguageTestFilter 'Ruby where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideRuby keepTests tokens
instance LanguageTestFilter 'PHP where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsidePHP keepTests tokens
instance LanguageTestFilter 'Swift where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideSwift keepTests tokens
instance LanguageTestFilter 'ObjectiveC where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideSwift keepTests tokens -- Reuse Swift implementation
instance LanguageTestFilter 'R where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideR keepTests tokens
instance LanguageTestFilter 'Julia where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideJulia keepTests tokens
instance LanguageTestFilter 'Perl where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsidePerl keepTests tokens
instance LanguageTestFilter 'OCaml where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideOCaml keepTests tokens
instance LanguageTestFilter 'Erlang where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideErlang keepTests tokens
instance LanguageTestFilter 'Nim where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideNim keepTests tokens
instance LanguageTestFilter 'Clojure where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideClojure keepTests tokens
instance LanguageTestFilter 'D where
langFilter Nothing tokens = tokens
langFilter (Just keepTests) tokens = processOutsideD keepTests tokens
-- ------------------------------------------------------------------
-- Main Dispatcher Function (Runtime-to-Type bridge)
-- ------------------------------------------------------------------
filterTests :: FileType -> Maybe Bool -> [Token] -> [Token]
filterTests fileType flag tokens =
case fileType of
Rust -> langFilter @'Rust flag tokens
Go -> langFilter @'Go flag tokens
Java -> langFilter @'Java flag tokens
Kotlin -> langFilter @'Kotlin flag tokens
C -> langFilter @'C flag tokens
Cpp -> langFilter @'Cpp flag tokens
Python -> langFilter @'Python flag tokens
Zig -> langFilter @'Zig flag tokens
Javascript -> langFilter @'Javascript flag tokens
Scala -> langFilter @'Scala flag tokens
Haskell -> langFilter @'Haskell flag tokens
Csharp -> langFilter @'Csharp flag tokens
Fsharp -> langFilter @'Fsharp flag tokens
Dart -> langFilter @'Dart flag tokens
Elixir -> langFilter @'Elixir flag tokens
Ruby -> langFilter @'Ruby flag tokens
PHP -> langFilter @'PHP flag tokens
Swift -> langFilter @'Swift flag tokens
ObjectiveC -> langFilter @'ObjectiveC flag tokens
R -> langFilter @'R flag tokens
Julia -> langFilter @'Julia flag tokens
Perl -> langFilter @'Perl flag tokens
OCaml -> langFilter @'OCaml flag tokens
Erlang -> langFilter @'Erlang flag tokens
Nim -> langFilter @'Nim flag tokens
Clojure -> langFilter @'Clojure flag tokens
D -> langFilter @'D flag tokens
_ -> tokens
-- ------------------------------------------------------------------
-- Rust-Specific Implementation Helpers (Moved from old function)
-- ------------------------------------------------------------------
-- | (Rust) Helper: Processes tokens *outside* a test block.
processOutsideRust :: Bool -> [Token] -> [Token]
processOutsideRust _ [] = [] -- End of stream
processOutsideRust keepTests (t1:t2:t3:t4:t5:t6:t7:t8:t9:t10:ts)
-- Look for the exact sequence: #[cfg(test)] mod <name> {
| (isTokenOperator t1 && tToken t1 == "#") &&
(isTokenBracket t2 && tToken t2 == "[") &&
(isTokenIdentifier t3 && tToken t3 == "cfg") &&
(isTokenBracket t4 && tToken t4 == "(") &&
(isTokenIdentifier t5 && tToken t5 == "test") &&
(isTokenBracket t6 && tToken t6 == ")") &&
(isTokenBracket t7 && tToken t7 == "]") &&
(isTokenKeyword t8 && tToken t8 == "mod") &&
isTokenIdentifier t9 && -- Module name (any identifier)
(isTokenBracket t10 && tToken t10 == "{")
=
-- Found the start of a test block.
-- Find the matching closing brace.
let (insideTokens, remainingTokens) = processInsideBraces 1 ts
in if keepTests
then -- We want test tokens, so keep the *entire* block
t1:t2:t3:t4:t5:t6:t7:t8:t9:t10:insideTokens ++ processOutsideRust keepTests remainingTokens
else -- We don't want test tokens, so discard the entire block
processOutsideRust keepTests remainingTokens
-- No test block marker found, process the current token
processOutsideRust keepTests (t:ts) =
if keepTests
then -- We want test tokens, so discard this "outside" token
processOutsideRust keepTests ts
else -- We don't want test tokens, so keep this "outside" token
t : processOutsideRust keepTests ts
-- ------------------------------------------------------------------
-- Go-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Go) Helper: Processes tokens *outside* a test function.
processOutsideGo :: Bool -> [Token] -> [Token]
processOutsideGo _ [] = [] -- End of stream
processOutsideGo keepTests (t1:t2:ts)
-- Look for "func Test..."
| isTokenKeyword t1 && tToken t1 == "func" &&
isTokenIdentifier t2 && "Test" `T.isPrefixOf` (tToken t2)
=
-- Found the start of a test function. Now find its opening brace.
case findOpeningBrace ts of
Nothing -> -- Malformed, no '{' found. Treat as non-test code.
if keepTests then processOutsideGo keepTests (t2:ts) else t1 : processOutsideGo keepTests (t2:ts)
Just (signatureTokens, tokensAfterBrace) ->
-- We found the function body's opening brace.
-- signatureTokens *includes* the opening brace.
-- tokensAfterBrace starts *after* the opening brace.
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then -- Keep func + name + signature + body
t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsideGo keepTests remainingTokens
else -- Discard the whole function
processOutsideGo keepTests remainingTokens
-- No test function found, process the current token
processOutsideGo keepTests (t:ts) =
if keepTests
then -- We want test tokens, so discard this "outside" token
processOutsideGo keepTests ts
else -- We don't want test tokens, so keep this "outside" token
t : processOutsideGo keepTests ts
-- ------------------------------------------------------------------
-- Java-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Java) Helper: Processes tokens *outside* a test method.
processOutsideJava :: Bool -> [Token] -> [Token]
processOutsideJava _ [] = [] -- End of stream
processOutsideJava keepTests (t1:t2:ts)
-- Look for "@Test"
| isTokenOperator t1 && tToken t1 == "@" &&
isTokenIdentifier t2 && tToken t2 == "Test"
=
-- Found @Test annotation. Now find its opening brace.
case findOpeningBrace ts of
Nothing -> -- Malformed, no '{' found. Treat as non-test code.
if keepTests then processOutsideJava keepTests (t2:ts) else t1 : processOutsideJava keepTests (t2:ts)
Just (signatureTokens, tokensAfterBrace) ->
-- We found the method body's opening brace.
-- signatureTokens *includes* the opening brace.
-- tokensAfterBrace starts *after* the opening brace.
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then -- Keep @Test + signature + body
t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsideJava keepTests remainingTokens
else -- Discard the whole method
processOutsideJava keepTests remainingTokens
-- No test method found, process the current token
processOutsideJava keepTests (t:ts) =
if keepTests
then -- We want test tokens, so discard this "outside" token
processOutsideJava keepTests ts
else -- We don't want test tokens, so keep this "outside" token
t : processOutsideJava keepTests ts
-- ------------------------------------------------------------------
-- C/C++-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (C/C++) Helper: Processes tokens *outside* a test function.
-- Recognizes:
-- 1. Functions starting with "test_" (snake_case, common in C)
-- 2. Functions starting with "Test" (PascalCase, common in C++)
-- 3. TEST(...) macro (Google Test)
-- 4. TEST_F(...) macro (Google Test with fixture)
-- 5. TEST_CASE(...) macro (Catch2)
processOutsideC :: Bool -> [Token] -> [Token]
processOutsideC _ [] = [] -- End of stream
-- Pattern 1: TEST( macro (Google Test)
processOutsideC keepTests (t1:t2:ts)
| isTokenIdentifier t1 && tToken t1 == "TEST" &&
isTokenBracket t2 && tToken t2 == "("
=
-- Found TEST( macro. Find the opening brace of the test body.
case findOpeningBrace ts of
Nothing -> -- Malformed, no '{' found. Treat as non-test code.
if keepTests then processOutsideC keepTests (t2:ts) else t1 : processOutsideC keepTests (t2:ts)
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsideC keepTests remainingTokens
else processOutsideC keepTests remainingTokens
-- Pattern 2: TEST_F( macro (Google Test with fixture)
processOutsideC keepTests (t1:t2:ts)
| isTokenIdentifier t1 && tToken t1 == "TEST_F" &&
isTokenBracket t2 && tToken t2 == "("
=
case findOpeningBrace ts of
Nothing ->
if keepTests then processOutsideC keepTests (t2:ts) else t1 : processOutsideC keepTests (t2:ts)
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsideC keepTests remainingTokens
else processOutsideC keepTests remainingTokens
-- Pattern 3: TEST_CASE( macro (Catch2)
processOutsideC keepTests (t1:t2:ts)
| isTokenIdentifier t1 && tToken t1 == "TEST_CASE" &&
isTokenBracket t2 && tToken t2 == "("
=
case findOpeningBrace ts of
Nothing ->
if keepTests then processOutsideC keepTests (t2:ts) else t1 : processOutsideC keepTests (t2:ts)
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsideC keepTests remainingTokens
else processOutsideC keepTests remainingTokens
-- Pattern 4: Function starting with "test_" or "Test"
processOutsideC keepTests (t1:ts)
| isTokenIdentifier t1 &&
(("test_" `T.isPrefixOf` tToken t1) || ("Test" `T.isPrefixOf` tToken t1))
=
-- Found a test function. Find the opening brace.
case findOpeningBrace ts of
Nothing -> -- Malformed, no '{' found. Treat as non-test code.
if keepTests then processOutsideC keepTests ts else t1 : processOutsideC keepTests ts
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : signatureTokens ++ bodyTokens ++ processOutsideC keepTests remainingTokens
else processOutsideC keepTests remainingTokens
-- No test found, process the current token
processOutsideC keepTests (t:ts) =
if keepTests
then -- We want test tokens, so discard this "outside" token
processOutsideC keepTests ts
else -- We don't want test tokens, so keep this "outside" token
t : processOutsideC keepTests ts
-- ------------------------------------------------------------------
-- Python-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Python) Helper: Processes tokens *outside* a test function.
-- Recognizes:
-- 1. Functions starting with "test_" (pytest, unittest convention)
-- 2. Classes starting with "Test" (pytest convention)
-- 3. Decorators @pytest, @unittest, @pytest.mark.* (common test decorators)
--
processOutsidePython :: Bool -> [Token] -> [Token]
processOutsidePython _ [] = [] -- End of stream
-- Pattern 1: @pytest or @unittest decorator
processOutsidePython keepTests (t1:t2:ts)
| isTokenOperator t1 && tToken t1 == "@" &&
isTokenIdentifier t2 && (tToken t2 == "pytest" || tToken t2 == "unittest")
=
-- Found a test decorator. Collect decorator line and the function/class.
let (decoratorTokens, afterDecorator) = collectDecoratorAndFunction ts
in if keepTests
then t1 : t2 : decoratorTokens ++ processOutsidePython keepTests afterDecorator
else processOutsidePython keepTests afterDecorator
-- Pattern 2: @pytest.mark.* decorator
processOutsidePython keepTests (t1:t2:t3:t4:t5:ts)
| isTokenOperator t1 && tToken t1 == "@" &&
isTokenIdentifier t2 && tToken t2 == "pytest" &&
isTokenOperator t3 && tToken t3 == "." &&
isTokenIdentifier t4 && tToken t4 == "mark" &&
isTokenOperator t5 && tToken t5 == "."
=
-- Found @pytest.mark.* decorator
let (decoratorTokens, afterDecorator) = collectDecoratorAndFunction ts
in if keepTests
then t1 : t2 : t3 : t4 : t5 : decoratorTokens ++ processOutsidePython keepTests afterDecorator
else processOutsidePython keepTests afterDecorator
-- Pattern 3: def test_*
processOutsidePython keepTests (t1:t2:ts)
| isTokenKeyword t1 && tToken t1 == "def" &&
isTokenIdentifier t2 && "test_" `T.isPrefixOf` tToken t2
=
-- Found a test function. Collect tokens until next def/class.
let (testTokens, remainingTokens) = collectUntilNextDefOrClass ts
in if keepTests
then t1 : t2 : testTokens ++ processOutsidePython keepTests remainingTokens
else processOutsidePython keepTests remainingTokens
-- Pattern 4: class Test*
processOutsidePython keepTests (t1:t2:ts)
| isTokenKeyword t1 && tToken t1 == "class" &&
isTokenIdentifier t2 && "Test" `T.isPrefixOf` tToken t2
=
-- Found a test class. Collect tokens until next class/def at same level.
let (testTokens, remainingTokens) = collectUntilNextDefOrClass ts
in if keepTests
then t1 : t2 : testTokens ++ processOutsidePython keepTests remainingTokens
else processOutsidePython keepTests remainingTokens
-- No test found, process the current token
processOutsidePython keepTests (t:ts) =
if keepTests
then -- We want test tokens, so discard this "outside" token
processOutsidePython keepTests ts
else -- We don't want test tokens, so keep this "outside" token
t : processOutsidePython keepTests ts
-- | Helper: Collect decorator tokens and the following function/class definition.
-- This handles decorators like @pytest.fixture, @unittest.skip, etc.
collectDecoratorAndFunction :: [Token] -> ([Token], [Token])
collectDecoratorAndFunction ts =
-- First, collect tokens until we hit 'def' or 'class'
let (beforeDef, fromDef) = collectUntilDefOrClass ts
in case fromDef of
[] -> (beforeDef, []) -- No def/class found
_ ->
-- Now collect the actual function/class body
let (bodyTokens, remaining) = collectUntilNextDefOrClass (drop 2 fromDef) -- skip 'def'/'class' and name
in (beforeDef ++ take 2 fromDef ++ bodyTokens, remaining)
-- | Helper: Collect tokens until we find 'def' or 'class' keyword.
collectUntilDefOrClass :: [Token] -> ([Token], [Token])
collectUntilDefOrClass [] = ([], [])
collectUntilDefOrClass (t:ts)
| isTokenKeyword t && (tToken t == "def" || tToken t == "class") =
([], t:ts) -- Found definition, stop here
| otherwise =
let (collected, remaining) = collectUntilDefOrClass ts
in (t : collected, remaining)
-- | Helper: Collect tokens until we find 'def' or 'class' keyword.
-- This is a simplified heuristic for Python's indentation-based blocks.
collectUntilNextDefOrClass :: [Token] -> ([Token], [Token])
collectUntilNextDefOrClass [] = ([], [])
collectUntilNextDefOrClass (t:ts)
| isTokenKeyword t && (tToken t == "def" || tToken t == "class") =
([], t:ts) -- Found next definition, stop here
| otherwise =
let (collected, remaining) = collectUntilNextDefOrClass ts
in (t : collected, remaining)
-- ------------------------------------------------------------------
-- Zig-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Zig) Helper: Processes tokens *outside* a test block.
-- Recognizes:
-- 1. test "description" { ... } - Zig's built-in test syntax
--
-- Zig has a very clean built-in test syntax that's easy to recognize.
processOutsideZig :: Bool -> [Token] -> [Token]
processOutsideZig _ [] = [] -- End of stream
-- Pattern: test "..." {
processOutsideZig keepTests (t1:ts)
| isTokenKeyword t1 && tToken t1 == "test"
=
-- Found a test block. Find the opening brace.
case findOpeningBrace ts of
Nothing -> -- Malformed, no '{' found. Treat as non-test code.
if keepTests then processOutsideZig keepTests ts else t1 : processOutsideZig keepTests ts
Just (signatureTokens, tokensAfterBrace) ->
-- signatureTokens includes the test name/description and the opening brace
-- tokensAfterBrace starts after the opening brace
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then -- Keep test keyword + description + body
t1 : signatureTokens ++ bodyTokens ++ processOutsideZig keepTests remainingTokens
else -- Discard the whole test block
processOutsideZig keepTests remainingTokens
-- No test found, process the current token
processOutsideZig keepTests (t:ts) =
if keepTests
then -- We want test tokens, so discard this "outside" token
processOutsideZig keepTests ts
else -- We don't want test tokens, so keep this "outside" token
t : processOutsideZig keepTests ts
-- ------------------------------------------------------------------
-- Javascript-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Javascript) Helper: Processes tokens *outside* a test block.
-- Recognizes:
-- 1. describe('...', function() { ... }) - Mocha/Jasmine/Jest
-- 2. describe('...', () => { ... }) - Modern syntax
-- 3. it('...', function() { ... }) - Test cases
-- 4. it('...', () => { ... }) - Modern syntax
-- 5. test('...', function() { ... }) - Jest
-- 6. test('...', () => { ... }) - Jest modern syntax
-- 7. context('...', ...) - Mocha/Jasmine
--
-- All these frameworks use similar patterns with function calls.
processOutsideJavascript :: Bool -> [Token] -> [Token]
processOutsideJavascript _ [] = [] -- End of stream
-- Pattern 1: describe( or it( or test( or context(
processOutsideJavascript keepTests (t1:t2:ts)
| isTokenIdentifier t1 &&
(tToken t1 == "describe" || tToken t1 == "it" || tToken t1 == "test" || tToken t1 == "context") &&
isTokenBracket t2 && tToken t2 == "("
=
-- Found a test function call. Find the opening brace of the test body.
case findOpeningBrace ts of
Nothing -> -- Malformed, no '{' found. Treat as non-test code.
if keepTests then processOutsideJavascript keepTests (t2:ts) else t1 : processOutsideJavascript keepTests (t2:ts)
Just (signatureTokens, tokensAfterBrace) ->
-- signatureTokens includes everything up to and including the opening brace
-- tokensAfterBrace starts after the opening brace
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then -- Keep test function call + signature + body
t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsideJavascript keepTests remainingTokens
else -- Discard the whole test block
processOutsideJavascript keepTests remainingTokens
-- No test found, process the current token
processOutsideJavascript keepTests (t:ts) =
if keepTests
then -- We want test tokens, so discard this "outside" token
processOutsideJavascript keepTests ts
else -- We don't want test tokens, so keep this "outside" token
t : processOutsideJavascript keepTests ts
-- ------------------------------------------------------------------
-- Scala-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Scala) Helper: Processes tokens *outside* a test block.
-- Recognizes:
-- 1. test("...") { ... } - ScalaTest FunSuite, MUnit
-- 2. it("...") { ... } - ScalaTest FunSpec
-- 3. describe("...") { ... } - ScalaTest FunSpec
-- 4. scenario("...") { ... } - ScalaTest FeatureSpec
-- 5. feature("...") { ... } - ScalaTest FeatureSpec
--
-- ScalaTest and MUnit are the most popular testing frameworks for Scala.
processOutsideScala :: Bool -> [Token] -> [Token]
processOutsideScala _ [] = [] -- End of stream
-- Pattern: test( or it( or describe( or scenario( or feature(
processOutsideScala keepTests (t1:t2:ts)
| isTokenIdentifier t1 &&
(tToken t1 == "test" || tToken t1 == "it" || tToken t1 == "describe" ||
tToken t1 == "scenario" || tToken t1 == "feature") &&
isTokenBracket t2 && tToken t2 == "("
=
-- Found a test function call. Find the opening brace of the test body.
case findOpeningBrace ts of
Nothing -> -- Malformed, no '{' found. Treat as non-test code.
if keepTests then processOutsideScala keepTests (t2:ts) else t1 : processOutsideScala keepTests (t2:ts)
Just (signatureTokens, tokensAfterBrace) ->
-- signatureTokens includes everything up to and including the opening brace
-- tokensAfterBrace starts after the opening brace
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then -- Keep test function call + signature + body
t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsideScala keepTests remainingTokens
else -- Discard the whole test block
processOutsideScala keepTests remainingTokens
-- No test found, process the current token
processOutsideScala keepTests (t:ts) =
if keepTests
then -- We want test tokens, so discard this "outside" token
processOutsideScala keepTests ts
else -- We don't want test tokens, so keep this "outside" token
t : processOutsideScala keepTests ts
-- ------------------------------------------------------------------
-- Haskell-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Haskell) Helper: Processes tokens *outside* a test block.
-- Recognizes:
-- 1. describe "..." - HSpec
-- 2. it "..." - HSpec
-- 3. context "..." - HSpec (alias for describe)
-- 4. testCase "..." - Tasty/HUnit
-- 5. testGroup "..." - Tasty
-- 6. testProperty "..." - Tasty/QuickCheck
-- 7. prop_* functions - QuickCheck convention
--
-- Haskell tests often use do-notation or $ operator, so we look for
-- these patterns and collect tokens until the next top-level definition.
processOutsideHaskell :: Bool -> [Token] -> [Token]
processOutsideHaskell _ [] = [] -- End of stream
-- Pattern 1: describe/it/context/testCase/testGroup/testProperty followed by string
processOutsideHaskell keepTests (t1:ts)
| isTokenIdentifier t1 &&
(tToken t1 == "describe" || tToken t1 == "it" || tToken t1 == "context" ||
tToken t1 == "testCase" || tToken t1 == "testGroup" || tToken t1 == "testProperty")
=
-- Found a test block. Try to find opening brace, otherwise collect until next definition.
case findOpeningBrace ts of
Just (signatureTokens, tokensAfterBrace) ->
-- Found braces, use them
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : signatureTokens ++ bodyTokens ++ processOutsideHaskell keepTests remainingTokens
else processOutsideHaskell keepTests remainingTokens
Nothing ->
-- No braces, collect until next top-level definition
let (testTokens, remainingTokens) = collectUntilNextHaskellDef ts
in if keepTests
then t1 : testTokens ++ processOutsideHaskell keepTests remainingTokens
else processOutsideHaskell keepTests remainingTokens
-- Pattern 2: prop_* function (QuickCheck convention)
processOutsideHaskell keepTests (t1:ts)
| isTokenIdentifier t1 && "prop_" `T.isPrefixOf` tToken t1
=
-- Found a property test function. Collect until next definition.
let (propTokens, remainingTokens) = collectUntilNextHaskellDef ts
in if keepTests
then t1 : propTokens ++ processOutsideHaskell keepTests remainingTokens
else processOutsideHaskell keepTests remainingTokens
-- No test found, process the current token
processOutsideHaskell keepTests (t:ts) =
if keepTests
then -- We want test tokens, so discard this "outside" token
processOutsideHaskell keepTests ts
else -- We don't want test tokens, so keep this "outside" token
t : processOutsideHaskell keepTests ts
-- | Helper: Collect tokens until we find the next top-level Haskell definition.
-- This looks for common patterns that indicate a new definition:
-- - describe, it, context, testCase, testGroup, testProperty (test keywords)
-- - main (main function)
-- - Keywords that start definitions at module level
collectUntilNextHaskellDef :: [Token] -> ([Token], [Token])
collectUntilNextHaskellDef [] = ([], [])
collectUntilNextHaskellDef (t:ts)
-- Found another test keyword or common top-level definition
| isTokenIdentifier t &&
(tToken t == "describe" || tToken t == "it" || tToken t == "context" ||
tToken t == "testCase" || tToken t == "testGroup" || tToken t == "testProperty" ||
tToken t == "main" || "prop_" `T.isPrefixOf` tToken t) =
([], t:ts) -- Found next definition, stop here
-- Found a type signature (identifier followed by ::)
| isTokenIdentifier t =
case ts of
(t2:_) | isTokenOperator t2 && tToken t2 == "::" ->
([], t:ts) -- Likely a new function definition
_ ->
let (collected, remaining) = collectUntilNextHaskellDef ts
in (t : collected, remaining)
| otherwise =
let (collected, remaining) = collectUntilNextHaskellDef ts
in (t : collected, remaining)
-- ------------------------------------------------------------------
-- C#-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (C#) Helper: Processes tokens *outside* a test method.
-- Recognizes:
-- 1. [Test] - NUnit
-- 2. [TestFixture] - NUnit
-- 3. [Fact] - xUnit
-- 4. [Theory] - xUnit
-- 5. [TestMethod] - MSTest
-- 6. [TestClass] - MSTest
--
-- C# uses attributes (annotations) similar to Java.
processOutsideCsharp :: Bool -> [Token] -> [Token]
processOutsideCsharp _ [] = [] -- End of stream
-- Pattern: [Test] or [Fact] or [Theory] or [TestMethod] etc.
processOutsideCsharp keepTests (t1:t2:t3:ts)
| isTokenBracket t1 && tToken t1 == "[" &&
isTokenIdentifier t2 &&
(tToken t2 == "Test" || tToken t2 == "TestFixture" || tToken t2 == "Fact" ||
tToken t2 == "Theory" || tToken t2 == "TestMethod" || tToken t2 == "TestClass") &&
isTokenBracket t3 && tToken t3 == "]"
=
-- Found a test attribute. Find the opening brace of the method/class.
case findOpeningBrace ts of
Nothing -> -- Malformed, no '{' found. Treat as non-test code.
if keepTests then processOutsideCsharp keepTests (t3:ts) else t1 : processOutsideCsharp keepTests (t2:t3:ts)
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : t2 : t3 : signatureTokens ++ bodyTokens ++ processOutsideCsharp keepTests remainingTokens
else processOutsideCsharp keepTests remainingTokens
-- No test found, process the current token
processOutsideCsharp keepTests (t:ts) =
if keepTests
then processOutsideCsharp keepTests ts
else t : processOutsideCsharp keepTests ts
-- ------------------------------------------------------------------
-- F#-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (F#) Helper: Processes tokens *outside* a test function.
-- Recognizes:
-- 1. [<Test>] - NUnit/xUnit (F# attribute syntax)
-- 2. [<Fact>] - xUnit
-- 3. [<Theory>] - xUnit
-- 4. testCase "..." - Expecto
-- 5. testList "..." - Expecto
-- 6. test "..." - Expecto
--
-- F# uses [< >] syntax for attributes and Expecto functions.
processOutsideFsharp :: Bool -> [Token] -> [Token]
processOutsideFsharp _ [] = [] -- End of stream
-- Pattern 1: [<Test>] or [<Fact>] or [<Theory>]
processOutsideFsharp keepTests (t1:t2:t3:t4:t5:ts)
| isTokenBracket t1 && tToken t1 == "[" &&
isTokenOperator t2 && tToken t2 == "<" &&
isTokenIdentifier t3 &&
(tToken t3 == "Test" || tToken t3 == "Fact" || tToken t3 == "Theory") &&
isTokenOperator t4 && tToken t4 == ">" &&
isTokenBracket t5 && tToken t5 == "]"
=
-- Found F# test attribute. Find the function body.
case findOpeningBrace ts of
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : t2 : t3 : t4 : t5 : signatureTokens ++ bodyTokens ++ processOutsideFsharp keepTests remainingTokens
else processOutsideFsharp keepTests remainingTokens
Nothing ->
-- No braces, collect until next definition
let (testTokens, remainingTokens) = collectUntilNextHaskellDef ts
in if keepTests
then t1 : t2 : t3 : t4 : t5 : testTokens ++ processOutsideFsharp keepTests remainingTokens
else processOutsideFsharp keepTests remainingTokens
-- Pattern 2: testCase or testList or test (Expecto)
processOutsideFsharp keepTests (t1:ts)
| isTokenIdentifier t1 &&
(tToken t1 == "testCase" || tToken t1 == "testList" || tToken t1 == "test")
=
case findOpeningBrace ts of
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : signatureTokens ++ bodyTokens ++ processOutsideFsharp keepTests remainingTokens
else processOutsideFsharp keepTests remainingTokens
Nothing ->
let (testTokens, remainingTokens) = collectUntilNextHaskellDef ts
in if keepTests
then t1 : testTokens ++ processOutsideFsharp keepTests remainingTokens
else processOutsideFsharp keepTests remainingTokens
-- No test found, process the current token
processOutsideFsharp keepTests (t:ts) =
if keepTests
then processOutsideFsharp keepTests ts
else t : processOutsideFsharp keepTests ts
-- ------------------------------------------------------------------
-- Dart-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Dart) Helper: Processes tokens *outside* a test block.
-- Recognizes:
-- 1. test('...') or test("...") - Dart test package
-- 2. group('...') or group("...") - Test group
-- 3. testWidgets('...') - Flutter widget tests
--
-- Similar to JavaScript but with Dart-specific functions.
processOutsideDart :: Bool -> [Token] -> [Token]
processOutsideDart _ [] = [] -- End of stream
-- Pattern: test( or group( or testWidgets(
processOutsideDart keepTests (t1:t2:ts)
| isTokenIdentifier t1 &&
(tToken t1 == "test" || tToken t1 == "group" || tToken t1 == "testWidgets") &&
isTokenBracket t2 && tToken t2 == "("
=
case findOpeningBrace ts of
Nothing ->
if keepTests then processOutsideDart keepTests (t2:ts) else t1 : processOutsideDart keepTests (t2:ts)
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsideDart keepTests remainingTokens
else processOutsideDart keepTests remainingTokens
-- No test found, process the current token
processOutsideDart keepTests (t:ts) =
if keepTests
then processOutsideDart keepTests ts
else t : processOutsideDart keepTests ts
-- ------------------------------------------------------------------
-- Elixir-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Elixir) Helper: Processes tokens *outside* a test block.
-- Recognizes:
-- 1. test "..." - ExUnit test
-- 2. describe "..." - ExUnit describe block
-- 3. defmodule *Test - Test module
--
-- Elixir uses ExUnit with do...end blocks.
processOutsideElixir :: Bool -> [Token] -> [Token]
processOutsideElixir _ [] = [] -- End of stream
-- Pattern 1: test or describe followed by string
processOutsideElixir keepTests (t1:ts)
| isTokenIdentifier t1 && (tToken t1 == "test" || tToken t1 == "describe")
=
-- Collect until we find 'end' keyword or next test/describe
let (testTokens, remainingTokens) = collectUntilElixirEnd ts
in if keepTests
then t1 : testTokens ++ processOutsideElixir keepTests remainingTokens
else processOutsideElixir keepTests remainingTokens
-- Pattern 2: defmodule *Test
processOutsideElixir keepTests (t1:t2:ts)
| isTokenKeyword t1 && tToken t1 == "defmodule" &&
isTokenIdentifier t2 && "Test" `T.isSuffixOf` tToken t2
=
-- Collect test module until 'end'
let (moduleTokens, remainingTokens) = collectUntilElixirEnd ts
in if keepTests
then t1 : t2 : moduleTokens ++ processOutsideElixir keepTests remainingTokens
else processOutsideElixir keepTests remainingTokens
-- No test found, process the current token
processOutsideElixir keepTests (t:ts) =
if keepTests
then processOutsideElixir keepTests ts
else t : processOutsideElixir keepTests ts
-- | Helper: Collect tokens until we find 'end' keyword in Elixir.
-- This handles do...end blocks.
collectUntilElixirEnd :: [Token] -> ([Token], [Token])
collectUntilElixirEnd = go 0
where
go :: Int -> [Token] -> ([Token], [Token])
go _ [] = ([], [])
go depth (t:ts)
-- Found 'do' - increase depth
| isTokenKeyword t && tToken t == "do" =
let (collected, remaining) = go (depth + 1) ts
in (t : collected, remaining)
-- Found 'end' - decrease depth or finish
| isTokenKeyword t && tToken t == "end" =
if depth <= 1
then ([t], ts) -- Include final 'end' and finish
else let (collected, remaining) = go (depth - 1) ts
in (t : collected, remaining)
-- Other tokens
| otherwise =
let (collected, remaining) = go depth ts
in (t : collected, remaining)
-- ------------------------------------------------------------------
-- Ruby-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Ruby) Helper: Processes tokens *outside* a test block.
-- Recognizes:
-- 1. describe "..." - RSpec
-- 2. context "..." - RSpec (alias for describe)
-- 3. it "..." - RSpec
-- 4. def test_* - Minitest
-- 5. class Test* - Minitest
--
-- Ruby uses RSpec (BDD) and Minitest frameworks.
processOutsideRuby :: Bool -> [Token] -> [Token]
processOutsideRuby _ [] = [] -- End of stream
-- Pattern 1: describe/context/it (RSpec)
processOutsideRuby keepTests (t1:ts)
| isTokenIdentifier t1 &&
(tToken t1 == "describe" || tToken t1 == "context" || tToken t1 == "it")
=
-- Collect until 'end' keyword
let (testTokens, remainingTokens) = collectUntilElixirEnd ts -- Reuse Elixir helper
in if keepTests
then t1 : testTokens ++ processOutsideRuby keepTests remainingTokens
else processOutsideRuby keepTests remainingTokens
-- Pattern 2: def test_* (Minitest)
processOutsideRuby keepTests (t1:t2:ts)
| isTokenKeyword t1 && tToken t1 == "def" &&
isTokenIdentifier t2 && "test_" `T.isPrefixOf` tToken t2
=
let (testTokens, remainingTokens) = collectUntilElixirEnd ts
in if keepTests
then t1 : t2 : testTokens ++ processOutsideRuby keepTests remainingTokens
else processOutsideRuby keepTests remainingTokens
-- Pattern 3: class Test* (Minitest)
processOutsideRuby keepTests (t1:t2:ts)
| isTokenKeyword t1 && tToken t1 == "class" &&
isTokenIdentifier t2 && "Test" `T.isPrefixOf` tToken t2
=
let (testTokens, remainingTokens) = collectUntilElixirEnd ts
in if keepTests
then t1 : t2 : testTokens ++ processOutsideRuby keepTests remainingTokens
else processOutsideRuby keepTests remainingTokens
-- No test found, process the current token
processOutsideRuby keepTests (t:ts) =
if keepTests
then processOutsideRuby keepTests ts
else t : processOutsideRuby keepTests ts
-- ------------------------------------------------------------------
-- PHP-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (PHP) Helper: Processes tokens *outside* a test method.
-- Recognizes:
-- 1. @test annotation in docblock
-- 2. test* method naming
-- 3. class *Test
--
-- PHP uses PHPUnit framework.
processOutsidePHP :: Bool -> [Token] -> [Token]
processOutsidePHP _ [] = [] -- End of stream
-- Pattern 1: @test annotation (in comment/docblock)
processOutsidePHP keepTests (t1:t2:ts)
| isTokenOperator t1 && tToken t1 == "@" &&
isTokenIdentifier t2 && tToken t2 == "test"
=
case findOpeningBrace ts of
Nothing ->
if keepTests then processOutsidePHP keepTests (t2:ts) else t1 : processOutsidePHP keepTests (t2:ts)
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsidePHP keepTests remainingTokens
else processOutsidePHP keepTests remainingTokens
-- Pattern 2: function/method starting with test
processOutsidePHP keepTests (t1:t2:ts)
| isTokenKeyword t1 && (tToken t1 == "function" || tToken t1 == "public" || tToken t1 == "protected") &&
isTokenIdentifier t2 && "test" `T.isPrefixOf` tToken t2
=
case findOpeningBrace ts of
Nothing ->
if keepTests then processOutsidePHP keepTests (t2:ts) else t1 : processOutsidePHP keepTests (t2:ts)
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsidePHP keepTests remainingTokens
else processOutsidePHP keepTests remainingTokens
-- Pattern 3: class *Test
processOutsidePHP keepTests (t1:t2:ts)
| isTokenKeyword t1 && tToken t1 == "class" &&
isTokenIdentifier t2 && "Test" `T.isSuffixOf` tToken t2
=
case findOpeningBrace ts of
Nothing ->
if keepTests then processOutsidePHP keepTests (t2:ts) else t1 : processOutsidePHP keepTests (t2:ts)
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsidePHP keepTests remainingTokens
else processOutsidePHP keepTests remainingTokens
-- No test found, process the current token
processOutsidePHP keepTests (t:ts) =
if keepTests
then processOutsidePHP keepTests ts
else t : processOutsidePHP keepTests ts
-- ------------------------------------------------------------------
-- Swift/Objective-C-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Swift/Objective-C) Helper: Processes tokens *outside* a test method.
-- Recognizes:
-- 1. class *Test: XCTestCase
-- 2. func test*()
--
-- Swift and Objective-C use XCTest framework.
processOutsideSwift :: Bool -> [Token] -> [Token]
processOutsideSwift _ [] = [] -- End of stream
-- Pattern 1: func test*
processOutsideSwift keepTests (t1:t2:ts)
| isTokenKeyword t1 && tToken t1 == "func" &&
isTokenIdentifier t2 && "test" `T.isPrefixOf` tToken t2
=
case findOpeningBrace ts of
Nothing ->
if keepTests then processOutsideSwift keepTests (t2:ts) else t1 : processOutsideSwift keepTests (t2:ts)
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsideSwift keepTests remainingTokens
else processOutsideSwift keepTests remainingTokens
-- Pattern 2: class *Test (could also check for XCTestCase inheritance)
processOutsideSwift keepTests (t1:t2:ts)
| isTokenKeyword t1 && tToken t1 == "class" &&
isTokenIdentifier t2 && "Test" `T.isSuffixOf` tToken t2
=
case findOpeningBrace ts of
Nothing ->
if keepTests then processOutsideSwift keepTests (t2:ts) else t1 : processOutsideSwift keepTests (t2:ts)
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsideSwift keepTests remainingTokens
else processOutsideSwift keepTests remainingTokens
-- No test found, process the current token
processOutsideSwift keepTests (t:ts) =
if keepTests
then processOutsideSwift keepTests ts
else t : processOutsideSwift keepTests ts
-- ------------------------------------------------------------------
-- R-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (R) Helper: Processes tokens *outside* a test block.
-- Recognizes:
-- 1. test_that("...", { ... })
-- 2. describe("...", { ... })
-- 3. context("...", { ... })
--
-- R uses testthat framework.
processOutsideR :: Bool -> [Token] -> [Token]
processOutsideR _ [] = [] -- End of stream
-- Pattern: test_that( or describe( or context(
processOutsideR keepTests (t1:t2:ts)
| isTokenIdentifier t1 &&
(tToken t1 == "test_that" || tToken t1 == "describe" || tToken t1 == "context") &&
isTokenBracket t2 && tToken t2 == "("
=
case findOpeningBrace ts of
Nothing ->
if keepTests then processOutsideR keepTests (t2:ts) else t1 : processOutsideR keepTests (t2:ts)
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsideR keepTests remainingTokens
else processOutsideR keepTests remainingTokens
-- No test found, process the current token
processOutsideR keepTests (t:ts) =
if keepTests
then processOutsideR keepTests ts
else t : processOutsideR keepTests ts
-- ------------------------------------------------------------------
-- Julia-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Julia) Helper: Processes tokens *outside* a test block.
-- Recognizes:
-- 1. @testset "..." begin ... end
-- 2. @test expression
--
-- Julia uses Test standard library.
processOutsideJulia :: Bool -> [Token] -> [Token]
processOutsideJulia _ [] = [] -- End of stream
-- Pattern 1: @testset
processOutsideJulia keepTests (t1:t2:ts)
| isTokenOperator t1 && tToken t1 == "@" &&
isTokenIdentifier t2 && tToken t2 == "testset"
=
-- Collect until 'end' keyword
let (testTokens, remainingTokens) = collectUntilElixirEnd ts
in if keepTests
then t1 : t2 : testTokens ++ processOutsideJulia keepTests remainingTokens
else processOutsideJulia keepTests remainingTokens
-- Pattern 2: @test
processOutsideJulia keepTests (t1:t2:ts)
| isTokenOperator t1 && tToken t1 == "@" &&
isTokenIdentifier t2 && tToken t2 == "test"
=
-- Single line test, collect until newline or next statement
let (testTokens, remainingTokens) = collectUntilNextHaskellDef ts
in if keepTests
then t1 : t2 : testTokens ++ processOutsideJulia keepTests remainingTokens
else processOutsideJulia keepTests remainingTokens
-- No test found, process the current token
processOutsideJulia keepTests (t:ts) =
if keepTests
then processOutsideJulia keepTests ts
else t : processOutsideJulia keepTests ts
-- ------------------------------------------------------------------
-- Perl-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Perl) Helper: Processes tokens *outside* a test block.
-- Recognizes:
-- 1. Test files (*.t)
-- 2. subtest blocks
--
-- Perl uses Test::More, Test::Simple frameworks.
-- Note: Perl tests are less structured, so this is a simplified approach.
processOutsidePerl :: Bool -> [Token] -> [Token]
processOutsidePerl _ [] = [] -- End of stream
-- Pattern: subtest
processOutsidePerl keepTests (t1:ts)
| isTokenIdentifier t1 && tToken t1 == "subtest"
=
case findOpeningBrace ts of
Nothing ->
-- Collect until next top-level statement
let (testTokens, remainingTokens) = collectUntilNextHaskellDef ts
in if keepTests
then t1 : testTokens ++ processOutsidePerl keepTests remainingTokens
else processOutsidePerl keepTests remainingTokens
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : signatureTokens ++ bodyTokens ++ processOutsidePerl keepTests remainingTokens
else processOutsidePerl keepTests remainingTokens
-- No test found, process the current token
processOutsidePerl keepTests (t:ts) =
if keepTests
then processOutsidePerl keepTests ts
else t : processOutsidePerl keepTests ts
-- ------------------------------------------------------------------
-- OCaml-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (OCaml) Helper: Processes tokens *outside* a test block.
-- Recognizes:
-- 1. let test_* = ... - OUnit convention
-- 2. test_case - Alcotest
--
-- OCaml uses OUnit and Alcotest frameworks.
processOutsideOCaml :: Bool -> [Token] -> [Token]
processOutsideOCaml _ [] = [] -- End of stream
-- Pattern 1: let test_*
processOutsideOCaml keepTests (t1:t2:ts)
| isTokenKeyword t1 && tToken t1 == "let" &&
isTokenIdentifier t2 && "test_" `T.isPrefixOf` tToken t2
=
-- Collect until next let or end of block
let (testTokens, remainingTokens) = collectUntilNextHaskellDef ts
in if keepTests
then t1 : t2 : testTokens ++ processOutsideOCaml keepTests remainingTokens
else processOutsideOCaml keepTests remainingTokens
-- Pattern 2: test_case
processOutsideOCaml keepTests (t1:ts)
| isTokenIdentifier t1 && tToken t1 == "test_case"
=
let (testTokens, remainingTokens) = collectUntilNextHaskellDef ts
in if keepTests
then t1 : testTokens ++ processOutsideOCaml keepTests remainingTokens
else processOutsideOCaml keepTests remainingTokens
-- No test found, process the current token
processOutsideOCaml keepTests (t:ts) =
if keepTests
then processOutsideOCaml keepTests ts
else t : processOutsideOCaml keepTests ts
-- ------------------------------------------------------------------
-- Erlang-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Erlang) Helper: Processes tokens *outside* a test function.
-- Recognizes:
-- 1. *_test() - EUnit convention
-- 2. *_test_() - EUnit generator convention
--
-- Erlang uses EUnit framework.
processOutsideErlang :: Bool -> [Token] -> [Token]
processOutsideErlang _ [] = [] -- End of stream
-- Pattern: function ending with _test or _test_
processOutsideErlang keepTests (t1:t2:ts)
| isTokenIdentifier t1 &&
("_test" `T.isSuffixOf` tToken t1 || "_test_" `T.isSuffixOf` tToken t1) &&
isTokenBracket t2 && tToken t2 == "("
=
-- Find the function body (might use -> or after parameters)
case findOpeningBrace ts of
Nothing ->
-- Collect until next function definition
let (testTokens, remainingTokens) = collectUntilNextHaskellDef ts
in if keepTests
then t1 : t2 : testTokens ++ processOutsideErlang keepTests remainingTokens
else processOutsideErlang keepTests remainingTokens
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : t2 : signatureTokens ++ bodyTokens ++ processOutsideErlang keepTests remainingTokens
else processOutsideErlang keepTests remainingTokens
-- No test found, process the current token
processOutsideErlang keepTests (t:ts) =
if keepTests
then processOutsideErlang keepTests ts
else t : processOutsideErlang keepTests ts
-- ------------------------------------------------------------------
-- Nim-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Nim) Helper: Processes tokens *outside* a test block.
-- Recognizes:
-- 1. suite "..."
-- 2. test "..."
--
-- Nim uses unittest module.
processOutsideNim :: Bool -> [Token] -> [Token]
processOutsideNim _ [] = [] -- End of stream
-- Pattern: suite or test
processOutsideNim keepTests (t1:ts)
| isTokenIdentifier t1 && (tToken t1 == "suite" || tToken t1 == "test")
=
-- Collect until next suite/test or end
let (testTokens, remainingTokens) = collectUntilNextHaskellDef ts
in if keepTests
then t1 : testTokens ++ processOutsideNim keepTests remainingTokens
else processOutsideNim keepTests remainingTokens
-- No test found, process the current token
processOutsideNim keepTests (t:ts) =
if keepTests
then processOutsideNim keepTests ts
else t : processOutsideNim keepTests ts
-- ------------------------------------------------------------------
-- Clojure-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (Clojure) Helper: Processes tokens *outside* a test block.
-- Recognizes:
-- 1. (deftest ...)
-- 2. (testing ...)
--
-- Clojure uses clojure.test.
processOutsideClojure :: Bool -> [Token] -> [Token]
processOutsideClojure _ [] = [] -- End of stream
-- Pattern 1: (deftest
processOutsideClojure keepTests (t1:t2:ts)
| isTokenBracket t1 && tToken t1 == "(" &&
isTokenIdentifier t2 && tToken t2 == "deftest"
=
-- Collect until matching closing paren
let (testTokens, remainingTokens) = collectUntilMatchingParen 1 ts
in if keepTests
then t1 : t2 : testTokens ++ processOutsideClojure keepTests remainingTokens
else processOutsideClojure keepTests remainingTokens
-- Pattern 2: (testing
processOutsideClojure keepTests (t1:t2:ts)
| isTokenBracket t1 && tToken t1 == "(" &&
isTokenIdentifier t2 && tToken t2 == "testing"
=
let (testTokens, remainingTokens) = collectUntilMatchingParen 1 ts
in if keepTests
then t1 : t2 : testTokens ++ processOutsideClojure keepTests remainingTokens
else processOutsideClojure keepTests remainingTokens
-- No test found, process the current token
processOutsideClojure keepTests (t:ts) =
if keepTests
then processOutsideClojure keepTests ts
else t : processOutsideClojure keepTests ts
-- | Helper: Collect tokens until matching closing parenthesis
collectUntilMatchingParen :: Int -> [Token] -> ([Token], [Token])
collectUntilMatchingParen 0 ts = ([], ts)
collectUntilMatchingParen _ [] = ([], [])
collectUntilMatchingParen depth (t:ts)
| isTokenBracket t && tToken t == "(" =
let (collected, remaining) = collectUntilMatchingParen (depth + 1) ts
in (t : collected, remaining)
| isTokenBracket t && tToken t == ")" =
if depth == 1
then ([t], ts)
else let (collected, remaining) = collectUntilMatchingParen (depth - 1) ts
in (t : collected, remaining)
| otherwise =
let (collected, remaining) = collectUntilMatchingParen depth ts
in (t : collected, remaining)
-- ------------------------------------------------------------------
-- D-Specific Implementation Helpers
-- ------------------------------------------------------------------
-- | (D) Helper: Processes tokens *outside* a test block.
-- Recognizes:
-- 1. unittest { ... } - D's built-in test blocks
--
-- D has built-in unittest blocks.
processOutsideD :: Bool -> [Token] -> [Token]
processOutsideD _ [] = [] -- End of stream
-- Pattern: unittest {
processOutsideD keepTests (t1:ts)
| isTokenKeyword t1 && tToken t1 == "unittest"
=
case findOpeningBrace ts of
Nothing ->
if keepTests then processOutsideD keepTests ts else t1 : processOutsideD keepTests ts
Just (signatureTokens, tokensAfterBrace) ->
let (bodyTokens, remainingTokens) = processInsideBraces 1 tokensAfterBrace
in if keepTests
then t1 : signatureTokens ++ bodyTokens ++ processOutsideD keepTests remainingTokens
else processOutsideD keepTests remainingTokens
-- No test found, process the current token
processOutsideD keepTests (t:ts) =
if keepTests
then processOutsideD keepTests ts
else t : processOutsideD keepTests ts
-- ------------------------------------------------------------------
-- Generic Helper Functions
-- ------------------------------------------------------------------
-- | (Generic) Helper: Processes tokens *inside* a braced block, handling nesting.
-- Renamed from processInsideRust
processInsideBraces :: Int -> [Token] -> ([Token], [Token])
processInsideBraces 0 ts = ([], ts) -- Base case: found matching '}'
processInsideBraces _ [] = ([], []) -- Error case: unexpected EOF
processInsideBraces nestingLevel (t:ts)
-- Found a nested opening brace
| isTokenBracket t && tToken t == "{" =
let (nestedInside, remaining) = processInsideBraces (nestingLevel + 1) ts
in (t : nestedInside, remaining) -- Keep '{' as part of inside tokens
-- Found a closing brace
| isTokenBracket t && tToken t == "}" =
if nestingLevel == 1
then -- This is the final '}' we were looking for
([t], ts) -- Include the final '}' and return the rest
else -- This is a nested '}'
let (nestedInside, remaining) = processInsideBraces (nestingLevel - 1) ts
in (t : nestedInside, remaining) -- Keep '}'
-- Any other token inside the block
| otherwise =
let (nestedInside, remaining) = processInsideBraces nestingLevel ts
in (t : nestedInside, remaining) -- Keep the token
-- | Helper to find the first opening brace, returning tokens before/at brace, and tokens after.
-- Returns (tokens_including_brace, tokens_after_brace)
findOpeningBrace :: [Token] -> Maybe ([Token], [Token])
findOpeningBrace = go []
where
go :: [Token] -> [Token] -> Maybe ([Token], [Token])
go _ [] = Nothing -- Reached end without finding a brace
go acc (t:ts)
| isTokenBracket t && tToken t == "{" = Just (reverse (t:acc), ts)
| otherwise = go (t:acc) ts