001package gudusoft.gsqlparser.ir.common; 002 003import gudusoft.gsqlparser.TSourceToken; 004import gudusoft.gsqlparser.nodes.TParseTreeNode; 005 006/** 007 * Immutable source code anchor linking an IR node back to the original SQL/PL/SQL text. 008 * <p> 009 * Every IR edge and most IR nodes carry a SourceAnchor so that any analysis result 010 * can be traced back to the exact source code position. 011 */ 012public final class SourceAnchor { 013 014 /** Source file identifier (file path or hash). */ 015 public final String fileId; 016 017 /** Character offset (0-based). */ 018 public final int startOffset; 019 public final int endOffset; 020 021 /** Line position (1-based). */ 022 public final int startLine; 023 public final int startCol; 024 public final int endLine; 025 public final int endCol; 026 027 /** Associated statementKey (for alignment with existing DataFlow system). */ 028 public final String statementKey; 029 030 /** Source code snippet text (for report display, max 200 characters). */ 031 public final String snippet; 032 033 public SourceAnchor(String fileId, 034 int startOffset, int endOffset, 035 int startLine, int startCol, 036 int endLine, int endCol, 037 String statementKey, 038 String snippet) { 039 this.fileId = fileId; 040 this.startOffset = startOffset; 041 this.endOffset = endOffset; 042 this.startLine = startLine; 043 this.startCol = startCol; 044 this.endLine = endLine; 045 this.endCol = endCol; 046 this.statementKey = statementKey; 047 this.snippet = snippet; 048 } 049 050 /** 051 * Construct a SourceAnchor from an AST node's start/end tokens. 052 */ 053 public static SourceAnchor from(TParseTreeNode astNode) { 054 if (astNode == null) { 055 return null; 056 } 057 TSourceToken start = astNode.getStartToken(); 058 TSourceToken end = astNode.getEndToken(); 059 if (start == null || end == null) { 060 return null; 061 } 062 063 // TSourceToken.lineNo/columnNo/offset are long; cast to int for SourceAnchor 064 int startOff = (int) start.offset; 065 int endOff = (int) end.offset + (end.astext != null ? end.astext.length() : 0); 066 int startLn = (int) start.lineNo; 067 int startC = (int) start.columnNo; 068 int endLn = (int) end.lineNo; 069 int endC = (int) end.columnNo + (end.astext != null ? end.astext.length() : 0); 070 071 String snip = extractSnippet(start, end, 200); 072 073 return new SourceAnchor("", startOff, endOff, 074 startLn, startC, endLn, endC, 075 null, snip); 076 } 077 078 /** 079 * Construct a SourceAnchor from a single token. 080 */ 081 public static SourceAnchor fromToken(TSourceToken token) { 082 if (token == null) { 083 return null; 084 } 085 int len = token.astext != null ? token.astext.length() : 0; 086 String snip = token.astext != null ? token.astext : ""; 087 if (snip.length() > 200) { 088 snip = snip.substring(0, 197) + "..."; 089 } 090 return new SourceAnchor("", 091 (int) token.offset, (int) token.offset + len, 092 (int) token.lineNo, (int) token.columnNo, 093 (int) token.lineNo, (int) token.columnNo + len, 094 null, snip); 095 } 096 097 private static String extractSnippet(TSourceToken start, TSourceToken end, int maxLen) { 098 if (start == null) { 099 return ""; 100 } 101 StringBuilder sb = new StringBuilder(); 102 TSourceToken current = start; 103 while (current != null) { 104 if (current.astext != null) { 105 if (sb.length() > 0) { 106 sb.append(' '); 107 } 108 sb.append(current.astext); 109 } 110 if (sb.length() > maxLen) { 111 break; 112 } 113 if (current == end) { 114 break; 115 } 116 current = current.getNextTokenInChain(); 117 } 118 String result = sb.toString(); 119 if (result.length() > maxLen) { 120 result = result.substring(0, maxLen - 3) + "..."; 121 } 122 return result; 123 } 124 125 @Override 126 public String toString() { 127 return fileId + ":" + startLine + ":" + startCol + "-" + endLine + ":" + endCol; 128 } 129}