// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.structuralsearch;

import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.util.Disposer;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.structuralsearch.*;
import com.intellij.structuralsearch.impl.matcher.CompiledPattern;
import com.intellij.structuralsearch.impl.matcher.compiler.PatternCompiler;
import com.intellij.testFramework.IdeaTestUtil;
import com.intellij.testFramework.PlatformTestUtil;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * @author Maxim.Mossienko
 */
@SuppressWarnings("ALL")
public class JavaStructuralSearchTest extends StructuralSearchTestCase {
  private final Disposable myBeforeParentDisposeDisposable = Disposer.newDisposable();

  @Override
  protected void setUp() throws Exception {
    super.setUp();
    IdeaTestUtil.setProjectLanguageLevel(getProject(), LanguageLevel.JDK_16, myBeforeParentDisposeDisposable);
  }

  @Override
  protected void tearDown() throws Exception {
    try {
      Disposer.dispose(myBeforeParentDisposeDisposable);
    }
    catch (Throwable e) {
      addSuppressedException(e);
    }
    finally {
      super.tearDown();
    }
  }

  @Override
  protected Sdk getProjectJDK() {
    return IdeaTestUtil.getMockJdk18();
  }

  protected List<MatchResult> findMatches(@Language("JAVA") String in, String pattern) {
    return super.findMatches(in, pattern, JavaFileType.INSTANCE);
  }

  protected int findMatchesCount(@Language("JAVA") String in, String pattern) {
    return findMatchesCount(in, pattern, JavaFileType.INSTANCE);
  }

  public void testSearchExpressions() {
    final String s2 = "class X {{ ((dialog==null)? (dialog = new SearchDialog()): dialog).show(); }}";
    assertFalse("subexpr match", findMatchesCount(s2, "dialog = new SearchDialog()") == 0);

    final String s10 = """
      class X {{
        listener.add(new Runnable() { public void run() {} });
      }}""";
    assertEquals("search for new ", 0, findMatchesCount(s10, " new XXX()"));

    final String s12 = """
      class X {{
      new Runnable() {
        public void run() {
         matchContext.getSink().matchingFinished();
         }
      };
      }}""";
    assertEquals("search for anonymous classes", 1, findMatchesCount(s12, "new Runnable() {}"));

    String source = """
      import java.util.*;
      class X {{
        new ArrayList() {};
        new ArrayList();
        new ArrayList<String>();
        new ArrayList<String>() {};
      }}""";
    assertEquals("search for parameterized anonymous class", 1, findMatchesCount(source, "new '_A<'_B>() {}"));

    final String s53 = """
      class C {
         String a = System.getProperty("abcd");
        static { String s = System.getProperty(a); }
        static void b() { String s = System.getProperty(a); }
       }""";
    assertEquals("expr in def initializer", 3, findMatchesCount(s53, "System.getProperty('T)"));

    final String s55 = """
      class X {{
        a = b.class;
      }}""";
    assertEquals("a.class expression", 1, findMatchesCount(s55, "'T.class"));

    String complexCode = """
      class X {{
        interface I { void b(); } interface I2 extends I {} class I3 extends I {}
        class A implements I2 {  void b() {} } class B implements I3 { void b() {}}
        I2 a; I3 b; a.b(); b.b(); b.b(); A c; B d; c.b(); d.b(); d.b();
      }}""";
    assertEquals("expr type condition", 1, findMatchesCount(complexCode, "'t:[exprtype( I2 )].b();"));
    assertEquals("expr type condition 2", 5, findMatchesCount(complexCode, "'t:[!exprtype( I2 )].b();"));
    assertEquals("expr type condition 3", 2, findMatchesCount(complexCode, "'t:[exprtype( *I2 )].b();"));
    assertEquals("expr type condition 4", 4, findMatchesCount(complexCode, "'t:[!exprtype( *I2 )].b();"));

    String complexCode2 = "enum X { XXX, YYY }\n class C { static void ordinal() {} void test() { C c; c.ordinal(); c.ordinal(); X.XXX.ordinal(); } }";
    assertEquals("expr type condition with enums", 1,
                 findMatchesCount(complexCode2, "'t:[exprtype( *java\\.lang\\.Enum )].ordinal()"));

    final String in = """
      class X {{
        processInheritors(1,2,3,4);
        processInheritors(1,2,3);
        processInheritors(1,2,3,4,5,6);
      }}""";
    assertEquals("no smart detection of search target", 3,
                 findMatchesCount(in, "'_instance?.processInheritors('_param1{1,6});"));

    String someCode = "class X {{ a *= 2; a+=2; }}";
    assertEquals("Improper *= 2 search", 1, findMatchesCount(someCode, "a *= 2;"));

    String s1 = """
      class X {{
        Thread t = new Thread("my thread","my another thread") {
            public void run() {
                // do stuff
            }
        };
      }}""";
    assertEquals("Find inner class parameters", 2, findMatchesCount(s1, "new Thread('args) { '_Other* }"));

    String s3 = """
      class X {{
        Thread t = new Thread("my thread") {
            public void run() {
                // do stuff
            }
        };
      }}""";
    assertEquals("Find inner class by new", 1, findMatchesCount(s3, "new Thread('_args)"));

    String s5 = """
      class A {
        public static <T> T[] copy(T[] array, Class<T> aClass) {
          int i = (int)0;
          int b = (int)0;
          return (T[])array.clone();
        }
      }""";
    assertEquals("Find cast to array", 1, findMatchesCount(s5, "('_T[])'_expr"));

    String s6 = """
      import java.util.HashMap;
      class X {
        HashMap x() {
          x();
          return null;
        }
      }""";
    assertEquals("Find expression only once for method call", 1, findMatchesCount(s6, "'Clz:[exprtype( java.util.HashMap )]"));

    String s7 = """
      import java.math.BigDecimal;

      public class Prorator {
              public void prorate(BigDecimal[] array) {
                      // do nothing
              }
              public void prorate2(java.math.BigDecimal[] array) {
                      // do nothing
              }
              public void prorate(BigDecimal bd) {
                      // do nothing
              }

              public static void main(String[] args) {
                      BigDecimal[] something = new BigDecimal[2];
                      java.math.BigDecimal[] something2 = new BigDecimal[2];
                      something[0] = new BigDecimal(1.0);
                      something[1] = new BigDecimal(1.0);

                      Prorator prorator = new Prorator();

      // ---------------------------------------------------
      // the line below should've been found, in my opinion.
      // --------------------------------------------------
                      prorator.prorate(something);
                      prorator.prorate(something2);
                      prorator.prorate(something[0]);
                      prorator.prorate(something[1]);
                      prorator.prorate(something[0]);
              }
      }""";
    assertEquals("Find method call with array for parameter expr type", 2,
                 findMatchesCount(s7, "'_Instance.'_MethodCall:[regex( prorate )]('_Param:[exprtype( BigDecimal\\[\\] )]) "));

    String s13 = "class X {{ try { } catch(Exception e) { e.printStackTrace(); }}}";
    assertEquals("Find statement in catch", 1, findMatchesCount(s13, "'_Instance.'_MethodCall('_Parameter*)"));

    String s4 = """
      class X {{
        int time = 99;
        String str = time < 0 ? "" : "";
        String str2 = time < time ? "" : "";
      }}""";

    assertEquals("Find expressions mistaken for declarations by parser in block mode", 1,
                 findMatchesCount(s4, "time < time"));

    assertEquals("Find expressions mistaken for declarations by parser in block mode 2", 1,
                 findMatchesCount(s4, "time < 0"));

    assertEquals("Find expressions mistaken for declarations by parser in block mode 3", 1,
                 findMatchesCount(s4, "time < 0 ? '_a : '_b"));

    assertEquals("Find expressions mistaken for declarations by parser in block mode 4", 2,
                 findMatchesCount(s4, "'_a < '_b"));

    String s11 = """
      import java.io.*;
      class X {
        void m() throws IOException {
          try (InputStream in = null) {}
        }
      }""";
    assertEquals("Find expression inside try-with-resources", 1, findMatchesCount(s11, "null"));
  }

  public void testNewArrayExpressions() {
    String s9 = """
      class X {{
        int a[] = new int[] { 1,2,3,4};
        int b[] = { 2,3,4,5 };
        Object[] c = new Object[] { "", null};
        Object[] d = {null, null};
        Object[] e = {};
        Object[] f = new Object[]{};
        String[] g = new String[]{};
        String[] h = new String[]{new String()};
      }}""";

    assertEquals("Find new array expressions, but no array initializer expressions", 5,
                 findMatchesCount(s9, "new '_ []{ '_* }"));

    assertEquals("Find new int array expressions, including array initializer expressions", 2,
                 findMatchesCount(s9, "new int []{ '_* }"));

    assertEquals("Find new int array expressions, including array initializer expressions using variable ", 2,
                 findMatchesCount(s9, "new '_a?:int [] { '_* }"));

    assertEquals("Find all new array expressions, including array initializers", 8,
                 findMatchesCount(s9, "new '_? []{ '_* }"));

    assertEquals("Find new Object array expressions, including array initializer expressions", 4,
                 findMatchesCount(s9, "new Object[] { '_* }"));

    assertEquals("Find only array initializer expressions", 3,
                 findMatchesCount(s9, "new '_{0,0}[] { '_* }"));

    assertEquals("Find only int array initializer expressions", 1,
                 findMatchesCount(s9, "new '_{0,0}:int [] { '_* }"));

    assertEquals("Try to find String array initializer expressions", 0,
                 findMatchesCount(s9, "new '_{0,0}:String [] { '_* }"));

    assertEquals("Find empty array initializers", 2, findMatchesCount(s9, "new Object[] {}"));

    String arrays = """
      class X {{
        int[] a = new int[20];
        byte[] b = new @Q byte[30];
      }}""";
    assertEquals("Improper array search", 1, findMatchesCount(arrays, "new int['_a]"));
    assertEquals("Find array of primitives", 2, findMatchesCount(arrays, "new '_X['_a]"));

    String multiDimensional = """
      class X {{
        String[] s1 = {};
        String[] s2 = new String[]{};
        String[][] s3 = new String[][]{};
      }}""";
    assertEquals("Find 2 dimensional array", 1, findMatchesCount(multiDimensional, "new String[][]{}"));
    assertEquals("Find 1 dimensional arrays", 2, findMatchesCount(multiDimensional, "new String[]{}"));
    assertEquals("Find empty 1 dimensional arrays", 2, findMatchesCount(multiDimensional, "new String[0]"));

    String singleDimensional = """
      class X {{
        String[] ss1 = new String[0];
        String[][] ss2 = new String[0][];
      }}""";
    assertEquals("Find empty array", 1, findMatchesCount(singleDimensional, "new String[0]"));
  }

  public void testLiteral() {
    String s = """
      class A {
        static String a = 1;
        static String s = "aaa";
        static String s2;
      }""";
    assertEquals("Literal", 2, findMatchesCount(s, "static String '_FieldName = '_Init?:[!regex( \".*\" )];"));
    assertEquals("Literal, 2", 1, findMatchesCount(s, "static String '_FieldName = '_Init:[!regex( \".*\" )];"));
    assertEquals("String literal", 1, findMatchesCount(s, "\"'String\""));

    String source = """
      @SuppressWarnings("test") class A {
        @SuppressWarnings({"other", "test"}) String field;
      }""";
    assertEquals("String literal in annotation", 2, findMatchesCount(source, "\"test\""));

    String s2 = """
      class A {
        String a = "Alpha";
        String b = "Bravo";
        String c = "Charlie";
      }""";
    assertEquals("match literal contents", 1, findMatchesCount(s2, "\"'String:[regex( alpha )]\""));
    assertEquals("negate match literal contents", 2, findMatchesCount(s2, "\"'String:[!regex( alpha )]\""));
    assertEquals("match literal contents combined with other constraint", 1, findMatchesCount(s2, "\"'String:[regex( alpha ) && script( true )]\""));

    String s3 = """
      class A {
        int i = 0x20;
        char c = 'a';
        char d = 'A';
        char e = 'z';
      }""";
    assertEquals("match literal by value", 1, findMatchesCount(s3, "32"));
    assertEquals("match char with substitution", 3, findMatchesCount(s3, "\\''_x\\'"));
    assertEquals("string literal should not match char", 0, findMatchesCount(s3, "\"a\""));

    String s4 = """
      class X {
        String s = "\\n";
        String t = " ";
        String u = " ";
        String v = "";
      }""";
    assertEquals("match empty string", 1, findMatchesCount(s4, "\"\""));
    assertEquals("match space", 2, findMatchesCount(s4, "\" \""));
  }

  public void testCovariantArraySearch() {
    String s1 = "class X {{ String[] argv; }}";
    String s2 = "class X {{ String argv; }}";
    assertEquals("Find array types", 0, findMatchesCount(s1, "String argv;"));
    assertEquals("Find array types, 2", 0, findMatchesCount(s2, "String[] argv;"));
    assertEquals("Find array types, 3", 0, findMatchesCount(s2, "'T[] argv;"));
    assertEquals("Find array types, 3", 1, findMatchesCount(s1, "'T:*Object [] argv;"));

    String s11 = """
      class A {
        void main(String[] argv);
        void main(String argv[]);
        void main(String argv);
      }""";
    assertEquals("Find array covariant types", 2, findMatchesCount(s11, "'_t:[regex( *Object\\[\\] ) ] '_t2;"));
    assertEquals("Find array covariant types, 2", 2, findMatchesCount(s11, "'_t:[regex( *Object ) ] '_t2 [];"));
    assertEquals("Find array covariant types, 3", 1, findMatchesCount(s11, "'_t:[regex( *Object ) ] '_t2;"));
  }

  public void testFindArrayDeclarations() {
    String source = """
      class A {
        String ss[][];
        int f()[] {
          return null;
        }
      }""";

    String target = "String[][] '_s;";
    assertEquals("should find multi-dimensional c-style array declarations", 1, findMatchesCount(source, target));

    String target2 = "class '_A { int[] 'f(); }";
    assertEquals("should find c-style method return type declarations", 1, findMatchesCount(source, target2));

    String target3 = "class '_A { int 'f(); }";
    assertEquals("should not find methods with array return types",0, findMatchesCount(source, target3));

    String source2 = """
      class A {
        void y(int... i) {}
        void y(String... ss) {}
        void y(boolean b) {}
      }""";
    assertEquals("find ellipsis type 1", 1, findMatchesCount(source2, "String[] '_a;"));
    assertEquals("find ellipsis type 2", 1, findMatchesCount(source2, "int[] '_a;"));
    assertEquals("find ellipsis type 3", 1, findMatchesCount(source2, "class '_X { void '_m(int... '_a); }"));
    assertEquals("find ellipsis type 4", 2, findMatchesCount(source2, "'_T[] '_a;"));

    String source3 = """
      class A {
        private int[] is;
      }""";
    assertEquals("find primitive array 1", 1, findMatchesCount(source3, "int[] '_a;"));
    assertEquals("find primitive array 2", 1, findMatchesCount(source3, "'_T[] '_a;"));
    assertEquals("find primitive array 3", 1, findMatchesCount(source3, "'_T:[regex( int )][] '_a;"));
    assertEquals("find primitive array 4", 1, findMatchesCount(source3, "'_T:[regex( int\\[\\] )] '_a;"));
  }

  // @todo support back references (\1 in another reg exp or as field member)
  //private static final String s1002 = " setSSS( instance.getSSS() ); " +
  //                                    " setSSS( instance.SSS ); ";
  //private static final String s1003 = " 't:set(.+) ( '_.get't_1() ); ";
  //private static final String s1003_2 = " 't:set(.+) ( '_.'t_1 ); ";

  public void testSearchStatements() {
    final String s = """
      class X {{
        debug("In action performed:"+event);
        project = (Project)event.getDataContext().getData(DataConstants.PROJECT);
        CodeEditorManager.getInstance(project).commitAllToPsiFile();
        file = (PsiFile) event.getDataContext().getData("psi.File");
        ((dialog==null)?
          (dialog = new SearchDialog()):
          dialog
        ).show();
      }}""";
    assertEquals("statement search", 1, findMatchesCount(s, "((dialog==null)? (dialog = new SearchDialog()): dialog).show();"));

    final String s5 = """
      class X {
        { System.out.println();
          while(false) {
            do {
               pattern = pattern.getNextSibling();
            }
              while (pattern!=null && filterLexicalNodes(pattern));
          }
         do {
          pattern = pattern.getNextSibling();
         } while (pattern!=null && filterLexicalNodes(pattern));
         { {
           do {
             pattern = pattern.getNextSibling();
           } while (pattern!=null && filterLexicalNodes(pattern));
         } }
        }
      }""";
    assertEquals("several constructions match", 3, findMatchesCount(s5, """
      do {
        pattern = pattern.getNextSibling();
      } while (pattern!=null && filterLexicalNodes(pattern));"""));
    assertEquals("several constructions 2", 0, findMatchesCount(s5, """
      do {
        pattern.getNextSibling();
      }  while (pattern!=null && filterLexicalNodes(pattern));"""));

    final String s7 = """
      class X {{
       if (true) throw new UnsupportedPatternException(statement.toString());
       if (true) {
         throw new UnsupportedPatternException(statement.toString());
       }
      }}""";
    assertEquals("several constructions 3", 2, findMatchesCount(s7, """
      if (true) {
        throw new UnsupportedPatternException(statement.toString());
      }"""));
    assertEquals("several constructions 4", 2,
                 findMatchesCount(s7, "if (true) throw new UnsupportedPatternException(statement.toString());"));

    final String s1000 = """
      class X {
          { lastTest = "search for parameterized pattern";
            matches = testMatcher.findMatches(s14_1,s15, options);
            if (matches.size()!=2 ) return false;
      lastTest = "search for parameterized pattern";
            matches = testMatcher.findMatches(s14_1,s15, options);
            if (matches.size()!=2 ) return false; }
      }""";
    final String s1001 = """
      lastTest = '_Descr;
            matches = testMatcher.findMatches('_In,'_Pattern, options);
            if (matches.size()!='_Number ) return false;""";
    assertEquals("several operators 5", 2, findMatchesCount(s1000, s1001));

    final String s85 = "class X {{ int a; a=1; a=1; return a; }}";
    assertEquals("two the same statements search", 1, findMatchesCount(s85, "'T; 'T;"));
    assertEquals("simple statement search (ignoring whitespace)", 4, findMatchesCount(s85, "'T ;"));

    final String s87 = "class X {{ getSomething(\"2\"); getSomething(\"1\"); a.call(); }}";
    assertEquals("search for simple call", 1, findMatchesCount(s87, " '_Instance.'Call('_*); "));
    assertEquals("search for simple call 2", 3, findMatchesCount(s87, " 'Call('_*); "));
    assertEquals("search for simple call 3", 3, findMatchesCount(s87, " '_Instance?.'Call('_*); "));
    assertEquals("search for simple call 4", 2, findMatchesCount(s87, " '_Instance{0,0}.'Call('_*); "));

    String s10015 = "class X {{ DocumentListener[] listeners = getCachedListeners(); }}";
    assertEquals("search for definition with init", 1, findMatchesCount(s10015, "'_Type 'Var = '_Call();"));

    String s10017 = "class X {{ a = b; b = c; a=a; c=c; }}";
    assertEquals("search silly assignments", 2, findMatchesCount(s10017, "'_a = '_a;"));

    String s10019 = "class X {{ a.b(); a.b(null); a.b(null, 1); }}";
    assertEquals("search parameter", 1, findMatchesCount(s10019, "a.b(null);"));

    String s1008 = "class X {{ int a, b, c, d; int a,b,c; int c,d; int e; }}";
    assertEquals("search many declarations", 2, findMatchesCount(s1008, "int '_a{3,4};"));

    String s1 = "class X {{ super(1,1);  call(1,1); call(2,2); }}";
    assertEquals("search super", 1, findMatchesCount(s1, "super('_t*);"));

    String s10021 = """
      class X {{
      short a = 1;
      short b = 2;
      short c = a.b();
      }}""";

    assertEquals("search def init bug", 1, findMatchesCount(s10021, "short '_a = '_b.b();"));

    String s10023 = """
      class X {{
        abstract class A { public abstract short getType(); }
        A a;
        switch(a.getType()) {
          default:
          return 0;
        }
        switch(a.getType()) {
          case 1:
          { return 0; }
        }
      }}""";
    assertEquals("finding switch", 2,
                 findMatchesCount(s10023, "switch('_a:[exprtype( short )]) { '_statement*; }"));

    String s10025 = """
      class X {{
        A[] a;
        A b[];
        A c;
      }}""";
    assertEquals("array types in dcl", 2, findMatchesCount(s10025, "A[] 'a;"));
    assertEquals("array types in dcl 2", 2, findMatchesCount(s10025, "A 'a[];"));

    String s10027 = """
      class X {{
        try { a(); } catch(Exception ex) {}
        try { a(); } finally {}
        try { a(); } catch(Exception ex) {} finally {}
      }}""";
    assertEquals("finally matching", 2, findMatchesCount(s10027, "try { a(); } finally {}\n"));

    String s10029 = "class X {{ for(String a:b) { System.out.println(a); }}}";
    assertEquals("for each matching", 1, findMatchesCount(s10029, "for(String a:b) { '_a; }"));

    String s10031 = """
      class X {{
        try { a(); } catch(Exception ex) {} catch(Error error) { 1=1; }
        try { a(); } catch(Exception ex) {}
      }}""";
    assertEquals("catch parameter matching", 3,
                 findMatchesCount(s10031, "try { a(); } catch('_Type 'Arg) { '_Statements*; }\n"));

    String s10033 = """
      class X {{
        return x;
        return !x;
        return (x);
        return (x);
        return !(x);
      }}""";
    assertEquals("Find statement with parenthesized expr",2,findMatchesCount(s10033, "return ('a);"));
    assertEquals("Find statement ignoring parentheses expr 2", 3, findMatchesCount(s10033, "return 'a:[regex( x )];"));

    String in = """
      class X {{
        if (true) {
          System.out.println();
        } else {
          System.out.println();
        }
        if (true) System.out.println();
      }}""";
    assertEquals("Find if statement with else", 2, findMatchesCount(in, "if ('_exp) { '_statement*; }"));
    assertEquals("Find if statement without else", 1,
                 findMatchesCount(in, "if ('_exp) { '_statement*; } else { '_statement2{0,0}; }"));

    String in2 = """
      /**
       * javadoc
      */
      class A {
        /* comment */

        void a() {
          System.out.println();
          // comment
        }
      }""";
    assertEquals("Should find statements and comments in statement context only", 2, findMatchesCount(in2, "'_statement;"));

    String in3 = """
      class X {{
        new Object().hashCode();
        new Object().toString();
      }}""";
    assertEquals("Find typed expression statements", 1, findMatchesCount(in3, "'_expr:[exprtype( int )];"));

    String in4 = """
      class X {
        void x() {
          System.out.println();
          {}
          // comment
          switch (1) {
            case 1: {}
          }
        }
      }""";
    assertEquals("block statement is a statement too", 1, findMatchesCount(in4, """
      void '_x() {
        '_st*;
      }"""));

    String in5 = """
      class X {
        void x() {
          while (true) {}
        }
      }""";
    assertEquals("match block statement with statement variable", 1, findMatchesCount(in5, "while (true) '_st;"));
  }

  public void testSearchClass() {
    final String s43 = """
      interface A extends B { int B = 1; }
      interface D { public final static double e = 1; }
      interface E { final static ind d = 2; }
      interface F {  }""";
    assertEquals("no modifier for interface vars", 3, findMatchesCount(s43, "interface '_ { '_T 'T2 = '_T3; } "));

    final String s45 = """
      class A extends B { private static final int B = 1; }
      class C extends D { int B = 1; }
      class E { }""";
    assertEquals("different order of access modifiers", 1, findMatchesCount(s45, "class '_ { final static private '_T 'T2 = '_T3; } "));
    assertEquals("no access modifier", 2, findMatchesCount(s45, "class '_ { '_T 'T2 = '_T3; } "));

    final String s47 = "class C { java.lang.String t; } class B { BufferedString t2;} class A { String p;} ";
    assertEquals("type differs with package", 2, findMatchesCount(s47, "class '_ { String '_; }"));

    final String s49 = "class C { void a() throws java.lang.RuntimeException {} } class B { BufferedString t2;}";
    assertEquals("reference could differ in package", 1, findMatchesCount(s49, "class '_ { '_ '_() throws RuntimeException; }"));

    String s51 = "class C extends java.awt.List {} class A extends java.util.List {} class B extends java.awt.List {} ";
    assertEquals("reference could differ in package 2", 2, findMatchesCount(s51, "class 'B extends '_C:java\\.awt\\.List {}"));

    final String s93 = """
      class A {
        private int field;
        public void b() {}
      }""";
    assertEquals("method access modifier", 0, findMatchesCount(s93, " class '_ {private void b() {}}"));
    assertEquals("method access modifier 2", 1, findMatchesCount(s93, " class '_ {public void b() {}}"));
    assertEquals("field access modifier", 0, findMatchesCount(s93, " class '_ {protected int field;}"));
    assertEquals("field access modifier 2", 1, findMatchesCount(s93, " class '_ {private int field;}"));

    final String s127 = "class a { void b() { new c() {}; } }";
    assertEquals("class finds anonymous class", 2, findMatchesCount(s127, "class 't {}"));

    final String s129 = """
      class a { public void run() {} }
      class a2 { public void run() { run(); } }
      class a3 { public void run() { run(); } }
      class a4 { public void run(); }""";

    assertEquals("empty method finds empty method only", 1,
                 findMatchesCount(s129, "class 'a { public void run() {} }"));
    assertEquals("nonempty method finds nonempty method", 2,
                 findMatchesCount(s129, "class 'a { public void run() { '_statement; } }"));
    assertEquals("nonempty method finds nonempty method", 4,
                 findMatchesCount(s129, "class 'a { public void run(); }"));

    final String s133 = """
      class S {
      void cc() {
              new Runnable() {
                  public void run() {
                      f();
                  }
                  private void f() {
                      //To change body of created methods use File | Settings | File Templates.
                  }
              };
              new Runnable() {
                  public void run() {
                      f();
                  }
                  private void g() {
                      //To change body of created methods use File | Settings | File Templates.
                  }
              };
              new Runnable() {
                  public void run() {
                      f();
                  }
              };
          }
          private void f() {
              //To change body of created methods use File | Settings | File Templates.
          }
      }""";
    final String s134 = """
      new Runnable() {
                  public void run() {
                      '_f ();
                  }
                  private void '_f ();
              }""";
    assertEquals(
      "complex expr matching",
      1,
      findMatchesCount(s133,s134)
    );

    final String s135 = """
      abstract class My {
          abstract void f();
      }
      abstract class My2 {
          abstract void f();
          void fg() {}
      }""";
    final String s136 = """
      class 'm {
          void f();
          '_type '_method{0,0} ('_paramtype '_paramname* );
      }""";
    assertEquals("reject method with 0 max occurence", 1, findMatchesCount(s135,s136));

    final String s137 = """
      abstract class My {
        int a;
      }
      abstract class My2 {
          Project b;
      }
      abstract class My3 {
          Class clazz;    Project b = null;
      }
      abstract class My {
        int a = 1;
      }
      """;
    assertEquals("reject field with 0 max occurence", 2,
                 findMatchesCount(s137, "class 'm { Project '_f{0,0} = '_t?; }"));

    final String s139 = "class My { boolean equals(Object o); int hashCode(); }";
    final String s140 = "class 'A { boolean equals(Object '_o ); int '_hashCode{0,0}:hashCode (); }";
    assertEquals("reject method with constraint", 0, findMatchesCount(s139,s140));

    final String s139_2 = "class My { boolean equals(Object o); }";
    assertEquals("reject field with 0 max occurence", 1, findMatchesCount(s139_2,s140));

    final String s141 = """
      class A { static { a = 10; } }
      class B { { a = 10; } }
      class C { { a = 10; } }""";
    assertEquals("static block search", 1, findMatchesCount(s141, "class '_ { static { a = 10; } } "));

    final String in = "class D<T> {}\n" +
                      "class T {}";
    assertEquals("search for class should not find type parameters", 2, findMatchesCount(in, "class 'A {}"));
  }

  public void testParameterlessConstructorSearch() {
    final String s143 = """
      class A { A() {} };
      class B { B(int a) {} };
      class C { C() {} C(int a) {} };
      class D { void method() {} }
      class E {}""";
    assertEquals("parameterless constructor search", 3,
                 findMatchesCount(s143, "class '_a { '_d{0,0}:[ script( \"__context__.constructor\" ) ]('_b '_c+); }"));
    assertEquals("parameterless constructor search 2", 2,
                 findMatchesCount(s143, "'_Constructor() { '_st*; }"));
    assertEquals("method & constructor search", 5,
                 findMatchesCount(s143, "'_T? '_identifier('_PT '_p*);"));
  }

  public void testScriptSearch() {
    final String source = """
      package a;
      class BX extends java.util.List {
        private static final java.util.List VALUE = new BX();
      }
      class CX extends java.util.List {
        private static final String S = "";
      }""";
    // find static final fields whose type is a proper ancestor of the class declaring their fields
    assertEquals("all variables accessible from script", 1,
                 findMatchesCount(source,
                                  "[script(\""                                                         +
                                  "import com.intellij.psi.util.InheritanceUtil\n"                     +
                                  "import com.intellij.psi.util.PsiTreeUtil\n"                         +
                                  "import com.intellij.psi.PsiClass\n"                                 +
                                  "init != null &&"                                                    + // redundant reference to '_init
                                  "InheritanceUtil.isInheritor(\n"                                     +
                                  "        PsiTreeUtil.getParentOfType(variable, PsiClass.class),\n"   + // reference to 'variable
                                  "        true, \n"                                                   +
                                  "        Type.type.canonicalText\n"                                  + // reference to '_Type
                                  ")\n\")]"                                                            +
                                  "static final '_Type 'variable = '_init;"));

    final String source2 = """
      class A {
        String s = new String();
        @SuppressWarnings("") int m() {
          n();
          int i = 2+1;
          return i;
        }
        void n() {}
      }""";
    assertEquals("type of variables in script are as expected", 1,
                 findMatchesCount(source2,
                                  """
                                    [script("
