001package gudusoft.gsqlparser.lineage.v2.producer; 002 003import java.util.Arrays; 004 005/** 006 * The decoded text of one source unit and the two coordinate systems that meet in it. 007 * 008 * <ul> 009 * <li><b>GSP's lexer</b> numbers lines and columns in UTF-16 code units, and ends a line at LF, CR LF or a 010 * lone CR ({@code TCustomLexer.readln}). Re-parsed text (routine bodies, dynamic SQL) has its token lines and 011 * columns shifted into the parent text while {@code TSourceToken.offset} stays relative to the inner text, so 012 * only the (line, column) pair is trusted: {@link #lexerOffset} turns it into a UTF-16 offset of this text.</li> 013 * <li><b>SQL-LR positions</b> ({@code Position}) count lines from 1 with LF as the line end, columns in code 014 * points from 1, and offsets in UTF-8 bytes from 0, as the reference validator checks them 015 * ({@code conformance/run.py}, M1 {@code SemanticStage.SourceText}). {@link #position} gives that triple for a 016 * UTF-16 offset.</li> 017 * </ul> 018 * 019 * <p>Immutable.</p> 020 * 021 * @since 4.2.17 022 */ 023public final class SourceText { 024 025 /** A SQL-LR position. */ 026 public static final class Position { 027 private final long line1; 028 private final long column1; 029 private final long utf8ByteOffset0; 030 031 Position(long line1, long column1, long utf8ByteOffset0) { 032 this.line1 = line1; 033 this.column1 = column1; 034 this.utf8ByteOffset0 = utf8ByteOffset0; 035 } 036 037 public long getLine1() { 038 return line1; 039 } 040 041 public long getColumn1() { 042 return column1; 043 } 044 045 public long getUtf8ByteOffset0() { 046 return utf8ByteOffset0; 047 } 048 049 @Override 050 public boolean equals(Object other) { 051 if (!(other instanceof Position)) { 052 return false; 053 } 054 Position p = (Position) other; 055 return line1 == p.line1 && column1 == p.column1 && utf8ByteOffset0 == p.utf8ByteOffset0; 056 } 057 058 @Override 059 public int hashCode() { 060 return (int) (line1 * 31 + column1) * 31 + (int) utf8ByteOffset0; 061 } 062 063 @Override 064 public String toString() { 065 return line1 + "," + column1 + "," + utf8ByteOffset0; 066 } 067 } 068 069 private final String text; 070 /** UTF-16 offset of the start of every line as GSP's lexer counts lines. */ 071 private final int[] lexerLineStarts; 072 /** UTF-16 offset of the start of every SQL-LR line (LF only). */ 073 private final int[] lineStarts; 074 /** UTF-8 byte offset before every UTF-16 offset; length text.length() + 1. */ 075 private final int[] utf8Before; 076 /** Code points before every UTF-16 offset; length text.length() + 1. */ 077 private final int[] codePointsBefore; 078 079 public SourceText(String text) { 080 if (text == null) { 081 throw new IllegalArgumentException("no text"); 082 } 083 this.text = text; 084 int n = text.length(); 085 int[] lexer = new int[16]; 086 int[] sqllr = new int[16]; 087 int lexerCount = 1; 088 int sqllrCount = 1; 089 utf8Before = new int[n + 1]; 090 codePointsBefore = new int[n + 1]; 091 int bytes = 0; 092 int points = 0; 093 for (int i = 0; i < n; i++) { 094 utf8Before[i] = bytes; 095 codePointsBefore[i] = points; 096 char c = text.charAt(i); 097 if (Character.isHighSurrogate(c) && i + 1 < n && Character.isLowSurrogate(text.charAt(i + 1))) { 098 bytes += 4; 099 points++; 100 utf8Before[i + 1] = bytes; // inside the pair: never a valid boundary, kept monotone 101 codePointsBefore[i + 1] = points; 102 i++; 103 continue; 104 } 105 if (Character.isSurrogate(c)) { 106 throw new IllegalArgumentException("text has an unpaired surrogate at UTF-16 offset " + i); 107 } 108 bytes += c < 0x80 ? 1 : c < 0x800 ? 2 : 3; 109 points++; 110 if (c == '\n') { 111 if (lexerCount == lexer.length) { 112 lexer = Arrays.copyOf(lexer, lexerCount * 2); 113 } 114 lexer[lexerCount++] = i + 1; 115 if (sqllrCount == sqllr.length) { 116 sqllr = Arrays.copyOf(sqllr, sqllrCount * 2); 117 } 118 sqllr[sqllrCount++] = i + 1; 119 } else if (c == '\r' && (i + 1 >= n || text.charAt(i + 1) != '\n')) { 120 if (lexerCount == lexer.length) { 121 lexer = Arrays.copyOf(lexer, lexerCount * 2); 122 } 123 lexer[lexerCount++] = i + 1; 124 } 125 } 126 utf8Before[n] = bytes; 127 codePointsBefore[n] = points; 128 lexerLineStarts = Arrays.copyOf(lexer, lexerCount); 129 lineStarts = Arrays.copyOf(sqllr, sqllrCount); 130 } 131 132 public String getText() { 133 return text; 134 } 135 136 /** The UTF-8 length of the text. */ 137 public int utf8Length() { 138 return utf8Before[text.length()]; 139 } 140 141 /** 142 * The UTF-16 offset of a lexer (line, column), or -1 when the pair lies outside the text. Column {@code c} 143 * of a line is the offset {@code lineStart + c - 1}; a column past the end of its line is outside. 144 */ 145 public int lexerOffset(long line1, long column1) { 146 if (line1 < 1 || line1 > lexerLineStarts.length || column1 < 1) { 147 return -1; 148 } 149 int start = lexerLineStarts[(int) line1 - 1]; 150 int end = line1 < lexerLineStarts.length ? lexerLineStarts[(int) line1] : text.length(); 151 if (column1 - 1 > end - start) { 152 return -1; 153 } 154 return start + (int) (column1 - 1); 155 } 156 157 /** 158 * The SQL-LR position of a UTF-16 offset, or {@code null} when the offset is outside the text or inside a 159 * surrogate pair. 160 */ 161 public Position position(int utf16Offset) { 162 if (utf16Offset < 0 || utf16Offset > text.length()) { 163 return null; 164 } 165 if (utf16Offset > 0 && utf16Offset < text.length() && Character.isLowSurrogate(text.charAt(utf16Offset)) 166 && Character.isHighSurrogate(text.charAt(utf16Offset - 1))) { 167 return null; 168 } 169 int low = 0; 170 int high = lineStarts.length - 1; 171 while (low < high) { 172 int mid = (low + high + 1) >>> 1; 173 if (lineStarts[mid] <= utf16Offset) { 174 low = mid; 175 } else { 176 high = mid - 1; 177 } 178 } 179 return new Position(low + 1, codePointsBefore[utf16Offset] - codePointsBefore[lineStarts[low]] + 1L, 180 utf8Before[utf16Offset]); 181 } 182 183 /** Whether {@code fragment} occurs in the text at {@code utf16Offset}. */ 184 public boolean matches(int utf16Offset, String fragment) { 185 return fragment != null && utf16Offset >= 0 && text.regionMatches(utf16Offset, fragment, 0, fragment.length()); 186 } 187}