@@ -2276,6 +2276,9 @@ private static bool ValidateScriptSyntax(string contents, ValidationLevel level,
22762276 return false ;
22772277 }
22782278
2279+ // Basic structural validation: check for duplicate method signatures
2280+ CheckDuplicateMethodSignatures ( contents , errorList ) ;
2281+
22792282#if USE_ROSLYN
22802283 // Advanced Roslyn-based validation: only run for Standard+; fail on Roslyn errors
22812284 if ( level >= ValidationLevel . Standard )
@@ -2518,6 +2521,57 @@ private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel
25182521 }
25192522#endif
25202523
2524+ private static int CountTopLevelParams ( string paramStr )
2525+ {
2526+ if ( string . IsNullOrWhiteSpace ( paramStr ) ) return 0 ;
2527+ int depth = 0 , count = 1 ;
2528+ foreach ( char c in paramStr )
2529+ {
2530+ if ( c == '<' || c == '(' || c == '[' ) depth ++ ;
2531+ else if ( c == '>' || c == ')' || c == ']' ) depth -- ;
2532+ else if ( c == ',' && depth == 0 ) count ++ ;
2533+ }
2534+ return count ;
2535+ }
2536+
2537+ /// <summary>
2538+ /// Extracts only the type portions from a parameter list, dropping parameter names.
2539+ /// e.g. "Dictionary<string, int> data, List<Vector3> points" → "Dictionary<string, int>, List<Vector3>"
2540+ /// </summary>
2541+ private static string ExtractParamTypes ( string paramStr )
2542+ {
2543+ if ( string . IsNullOrWhiteSpace ( paramStr ) ) return "" ;
2544+ var types = new System . Text . StringBuilder ( ) ;
2545+ // Split at top-level commas (respecting <> depth)
2546+ int depth = 0 , start = 0 ;
2547+ for ( int i = 0 ; i <= paramStr . Length ; i ++ )
2548+ {
2549+ char c = i < paramStr . Length ? paramStr [ i ] : ',' ; // sentinel
2550+ if ( c == '<' || c == '(' || c == '[' ) depth ++ ;
2551+ else if ( c == '>' || c == ')' || c == ']' ) depth -- ;
2552+ else if ( c == ',' && depth == 0 )
2553+ {
2554+ string param = paramStr . Substring ( start , i - start ) . Trim ( ) ;
2555+ if ( types . Length > 0 ) types . Append ( ", " ) ;
2556+ // The type is everything except the last token (the name).
2557+ // But if the last token ends with '>' or ']', it's all type (e.g. "List<int>").
2558+ // Find the last whitespace that is NOT inside <> brackets.
2559+ int lastSplit = - 1 ;
2560+ int d2 = 0 ;
2561+ for ( int j = 0 ; j < param . Length ; j ++ )
2562+ {
2563+ char pc = param [ j ] ;
2564+ if ( pc == '<' || pc == '(' || pc == '[' ) d2 ++ ;
2565+ else if ( pc == '>' || pc == ')' || pc == ']' ) d2 -- ;
2566+ else if ( d2 == 0 && char . IsWhiteSpace ( pc ) ) lastSplit = j ;
2567+ }
2568+ types . Append ( lastSplit > 0 ? param . Substring ( 0 , lastSplit ) . Trim ( ) : param ) ;
2569+ start = i + 1 ;
2570+ }
2571+ }
2572+ return Regex . Replace ( types . ToString ( ) , @"\s+" , " " ) ;
2573+ }
2574+
25212575 /// <summary>
25222576 /// Validates Unity-specific coding rules and best practices
25232577 /// //TODO: Naive Unity Checks and not really yield any results, need to be improved
@@ -2581,6 +2635,121 @@ private static void ValidateScriptSyntaxUnity(string contents, System.Collection
25812635 {
25822636 errors . Add ( "WARNING: String concatenation in Update() can cause garbage collection issues" ) ;
25832637 }
2638+
2639+ }
2640+
2641+ /// <summary>
2642+ /// Checks for duplicate method signatures (same name + param types + scope).
2643+ /// Catches corruption from anchor_replace overshoot and similar edit mistakes.
2644+ /// Runs at Basic level since duplicates are structural errors, not style warnings.
2645+ /// </summary>
2646+ private static void CheckDuplicateMethodSignatures ( string contents , System . Collections . Generic . List < string > errors )
2647+ {
2648+ // Step 1: Build a code-only view (comments/strings replaced with spaces)
2649+ var codeChars = contents . ToCharArray ( ) ;
2650+ {
2651+ var lexer = new CSharpLexer ( contents ) ;
2652+ while ( true )
2653+ {
2654+ int startPos = lexer . Position ;
2655+ if ( ! lexer . Advance ( out _ ) ) break ;
2656+ if ( lexer . InNonCode )
2657+ {
2658+ for ( int i = startPos ; i < lexer . Position && i < codeChars . Length ; i ++ )
2659+ if ( codeChars [ i ] != '\n ' ) codeChars [ i ] = ' ' ;
2660+ }
2661+ }
2662+ }
2663+ var codeOnly = new string ( codeChars ) ;
2664+
2665+ // Step 2: Build containing type name at each position via single pass
2666+ var typePattern = new Regex (
2667+ @"\b(?:class|struct|interface|record)\s+(\w+)" ,
2668+ RegexOptions . CultureInvariant , TimeSpan . FromSeconds ( 2 ) ) ;
2669+ // Map opening-brace position -> fully qualified type name
2670+ var typeBraceMap = new System . Collections . Generic . Dictionary < int , string > ( ) ;
2671+ {
2672+ var typeMatches = typePattern . Matches ( codeOnly ) ;
2673+ // Pre-scan: for each type declaration, find its opening brace and record full name
2674+ // We need to process in order and track nesting, so do a single forward pass
2675+ var preStack = new System . Collections . Generic . List < ( string name , int openDepth ) > ( ) ;
2676+ int preBd = 0 ;
2677+ int nextTm = 0 ;
2678+ for ( int i = 0 ; i < codeOnly . Length ; i ++ )
2679+ {
2680+ while ( nextTm < typeMatches . Count && typeMatches [ nextTm ] . Index == i )
2681+ {
2682+ int bp = codeOnly . IndexOf ( '{' , typeMatches [ nextTm ] . Index + typeMatches [ nextTm ] . Length ) ;
2683+ if ( bp >= 0 )
2684+ {
2685+ string tn = typeMatches [ nextTm ] . Groups [ 1 ] . Value ;
2686+ string fn = preStack . Count > 0 ? preStack [ preStack . Count - 1 ] . name + "." + tn : tn ;
2687+ typeBraceMap [ bp ] = fn ;
2688+ }
2689+ nextTm ++ ;
2690+ }
2691+ if ( codeOnly [ i ] == '{' )
2692+ {
2693+ if ( typeBraceMap . TryGetValue ( i , out string mappedType ) )
2694+ preStack . Add ( ( mappedType , preBd ) ) ;
2695+ preBd ++ ;
2696+ }
2697+ else if ( codeOnly [ i ] == '}' )
2698+ {
2699+ preBd = Math . Max ( 0 , preBd - 1 ) ;
2700+ if ( preStack . Count > 0 && preStack [ preStack . Count - 1 ] . openDepth == preBd )
2701+ preStack . RemoveAt ( preStack . Count - 1 ) ;
2702+ }
2703+ }
2704+ }
2705+ // Second pass: build per-position containing type array
2706+ var containingTypeArr = new string [ codeOnly . Length ] ;
2707+ {
2708+ var stack = new System . Collections . Generic . List < ( string name , int openDepth ) > ( ) ;
2709+ int bd2 = 0 ;
2710+ string current = "" ;
2711+ for ( int i = 0 ; i < codeOnly . Length ; i ++ )
2712+ {
2713+ if ( codeOnly [ i ] == '{' )
2714+ {
2715+ if ( typeBraceMap . TryGetValue ( i , out string tn ) )
2716+ {
2717+ stack . Add ( ( tn , bd2 ) ) ;
2718+ current = tn ;
2719+ }
2720+ bd2 ++ ;
2721+ }
2722+ else if ( codeOnly [ i ] == '}' )
2723+ {
2724+ bd2 = Math . Max ( 0 , bd2 - 1 ) ;
2725+ if ( stack . Count > 0 && stack [ stack . Count - 1 ] . openDepth == bd2 )
2726+ {
2727+ stack . RemoveAt ( stack . Count - 1 ) ;
2728+ current = stack . Count > 0 ? stack [ stack . Count - 1 ] . name : "" ;
2729+ }
2730+ }
2731+ containingTypeArr [ i ] = current ;
2732+ }
2733+ }
2734+
2735+ // Step 3: Match method signatures on code-only text (includes => for expression-bodied)
2736+ var methodSigPattern = new Regex (
2737+ @"(?:(?:public|private|protected|internal)\s+)?(?:(?:static|virtual|override|abstract|sealed|async|new)\s+)*\S+\s+(\w+)\s*\(([^)]*)\)\s*(?:where\s+\S+\s*:\s*\S+\s*)?(?:[{;]|=>)" ,
2738+ RegexOptions . Multiline | RegexOptions . CultureInvariant , TimeSpan . FromSeconds ( 2 ) ) ;
2739+ var sigMatches = methodSigPattern . Matches ( codeOnly ) ;
2740+ var seen = new System . Collections . Generic . Dictionary < string , int > ( System . StringComparer . Ordinal ) ;
2741+ foreach ( Match sm in sigMatches )
2742+ {
2743+ string methodName = sm . Groups [ 1 ] . Value ;
2744+ int paramCount = CountTopLevelParams ( sm . Groups [ 2 ] . Value ) ;
2745+ string paramTypes = ExtractParamTypes ( sm . Groups [ 2 ] . Value ) ;
2746+ string containingType = containingTypeArr [ sm . Index ] ;
2747+ string key = $ "{ containingType } /{ methodName } /{ paramCount } /{ paramTypes } ";
2748+ if ( seen . TryGetValue ( key , out _ ) )
2749+ errors . Add ( $ "ERROR: Duplicate method signature detected: '{ methodName } ' with { paramCount } parameter(s). This may indicate a corrupted edit.") ;
2750+ else
2751+ seen [ key ] = 1 ;
2752+ }
25842753 }
25852754
25862755 /// <summary>
0 commit comments