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}