cloudflare/pint
Publicmirrored from https://github.com/cloudflare/pintAvailable
internal/parser/source/position.go
210lines · modecode
| 1 | package source |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | |
| 6 | "github.com/prometheus/prometheus/model/labels" |
| 7 | promParser "github.com/prometheus/prometheus/promql/parser" |
| 8 | "github.com/prometheus/prometheus/promql/parser/posrange" |
| 9 | ) |
| 10 | |
| 11 | func GetQueryFragment(expr string, pos posrange.PositionRange) string { |
| 12 | return expr[pos.Start:pos.End] |
| 13 | } |
| 14 | |
| 15 | func isOutside(pos posrange.PositionRange, outside []posrange.PositionRange) bool { |
| 16 | for _, out := range outside { |
| 17 | if pos.Start >= out.Start && pos.End <= out.End { |
| 18 | return false |
| 19 | } |
| 20 | } |
| 21 | return true |
| 22 | } |
| 23 | |
| 24 | // FindFuncNamePosition returns the source position of a function name within |
| 25 | // the provided fragment. |
| 26 | // |
| 27 | // It matches fn case-insensitively, but only when the matched text is |
| 28 | // followed by optional whitespace and then `(`. This allows `by (...)` to |
| 29 | // match `by` while skipping the same text inside longer words such as |
| 30 | // `bytes`. |
| 31 | // |
| 32 | // The returned range covers only the function name. When converting it to a |
| 33 | // diagnostic, use: |
| 34 | // |
| 35 | // - FirstColumn = pos.Start + 1 |
| 36 | // - LastColumn = pos.End. |
| 37 | func FindFuncNamePosition(expr string, within posrange.PositionRange, fn string) posrange.PositionRange { |
| 38 | fragment := GetQueryFragment(expr, within) |
| 39 | lower := strings.ToLower(fragment) |
| 40 | fnLower := strings.ToLower(fn) |
| 41 | offset := 0 |
| 42 | for { |
| 43 | // Find next case-insensitive occurrence of fn. |
| 44 | idx := strings.Index(lower[offset:], fnLower) |
| 45 | if idx < 0 { |
| 46 | return within |
| 47 | } |
| 48 | idx += offset |
| 49 | end := idx + len(fn) |
| 50 | // Check that fn is followed by optional whitespace then '('. |
| 51 | // This skips false positives like "by" inside "bytes". |
| 52 | for i := end; i < len(fragment); i++ { |
| 53 | c := fragment[i] |
| 54 | if c == '(' { |
| 55 | return posrange.PositionRange{ |
| 56 | Start: within.Start + posrange.Pos(idx), |
| 57 | End: within.Start + posrange.Pos(end), |
| 58 | } |
| 59 | } |
| 60 | if c != ' ' && c != '\n' && c != '\t' { |
| 61 | break // Not whitespace, not '(' — this occurrence is inside a word. |
| 62 | } |
| 63 | } |
| 64 | offset = idx + 1 // Try next occurrence. |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | // FindFuncPosition returns the position of the whole `fn(...)` call inside the |
| 69 | // within fragment, from fn up to its closing ')'. When within spans more than |
| 70 | // one call, outside lists ranges the match must fall within, which lets callers |
| 71 | // pick the right `fn(...)` when the same keyword appears on both sides. |
| 72 | func FindFuncPosition(expr string, within posrange.PositionRange, fn string, outside []posrange.PositionRange) posrange.PositionRange { |
| 73 | fragment := GetQueryFragment(expr, within) |
| 74 | lower := strings.ToLower(fragment) |
| 75 | fnLower := strings.ToLower(fn) |
| 76 | for offset := 0; ; { |
| 77 | idx := strings.Index(lower[offset:], fnLower) |
| 78 | if idx < 0 { |
| 79 | return within |
| 80 | } |
| 81 | idx += offset |
| 82 | offset = idx + 1 |
| 83 | |
| 84 | // fn must be followed by optional whitespace and then '(', otherwise |
| 85 | // this is just fn appearing inside a longer word like "bytes". |
| 86 | open := -1 |
| 87 | for i := idx + len(fn); i < len(fragment); i++ { |
| 88 | c := fragment[i] |
| 89 | if c == '(' { |
| 90 | open = i |
| 91 | break |
| 92 | } |
| 93 | if c != ' ' && c != '\n' && c != '\t' { |
| 94 | break |
| 95 | } |
| 96 | } |
| 97 | if open < 0 { |
| 98 | continue |
| 99 | } |
| 100 | |
| 101 | // The call ends at the first ')' after the opening paren. |
| 102 | closeRel := strings.IndexByte(fragment[open:], ')') |
| 103 | if closeRel < 0 { |
| 104 | continue |
| 105 | } |
| 106 | |
| 107 | pos := posrange.PositionRange{ |
| 108 | Start: within.Start + posrange.Pos(idx), |
| 109 | End: within.Start + posrange.Pos(open+closeRel+1), |
| 110 | } |
| 111 | if isOutside(pos, outside) { |
| 112 | return pos |
| 113 | } |
| 114 | return within |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | // findArgumentPosition looks for name inside the expr substring selected by |
| 119 | // within and returns the matching argument token position in the original expr. |
| 120 | // |
| 121 | // For example, if expr is `sum(foo) by(job, instance) > 0` and within selects |
| 122 | // `by(job, instance)`, then name="instance" returns the position of |
| 123 | // `instance` in the full expr. |
| 124 | // |
| 125 | // If expr is `foo * on(job) group_left(cluster) bar` and within selects |
| 126 | // `on(job)`, then name="job" returns the position of `job` in the full expr. |
| 127 | // |
| 128 | // If expr is `sum(foo) without(notify)` and within selects `without(notify)`, |
| 129 | // then name="notify" returns the position of `notify` in the full expr. |
| 130 | func findArgumentPosition(expr string, within posrange.PositionRange, name string) posrange.PositionRange { |
| 131 | fragment := GetQueryFragment(expr, within) |
| 132 | |
| 133 | isSpace := func(b byte) bool { |
| 134 | return b == ' ' || b == '\n' || b == '\t' |
| 135 | } |
| 136 | // Match the right-most name that sits as a complete argument, i.e. one |
| 137 | // bordered by '(' or ',' on the left and ',' or ')' on the right. |
| 138 | best := -1 |
| 139 | for off := 0; ; { |
| 140 | i := strings.Index(fragment[off:], name) |
| 141 | if i < 0 { |
| 142 | break |
| 143 | } |
| 144 | i += off |
| 145 | off = i + 1 |
| 146 | |
| 147 | // The byte opening the argument must be '(' or a ',' separator. |
| 148 | p := i - 1 |
| 149 | for p >= 0 && isSpace(fragment[p]) { |
| 150 | p-- |
| 151 | } |
| 152 | if p < 0 || (fragment[p] != '(' && fragment[p] != ',') { |
| 153 | continue |
| 154 | } |
| 155 | |
| 156 | // The byte closing the argument must be ')' or a ',' separator. |
| 157 | q := i + len(name) |
| 158 | for q < len(fragment) && isSpace(fragment[q]) { |
| 159 | q++ |
| 160 | } |
| 161 | if q >= len(fragment) || (fragment[q] != ')' && fragment[q] != ',') { |
| 162 | continue |
| 163 | } |
| 164 | |
| 165 | best = i |
| 166 | } |
| 167 | if best < 0 { |
| 168 | return within |
| 169 | } |
| 170 | return posrange.PositionRange{ |
| 171 | Start: within.Start + posrange.Pos(best), |
| 172 | End: within.Start + posrange.Pos(best+len(name)), |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | // findBinOpsOperatorPosition returns the position of the op operator between the |
| 177 | // two sides of a binary expression, searching only the gap between them so a |
| 178 | // matching op inside either side is ignored. |
| 179 | func findBinOpsOperatorPosition(expr string, n *promParser.BinaryExpr, op string) posrange.PositionRange { |
| 180 | within := posrange.PositionRange{ |
| 181 | Start: n.LHS.PositionRange().End + 1, |
| 182 | End: n.RHS.PositionRange().Start, |
| 183 | } |
| 184 | idx := strings.Index(GetQueryFragment(expr, within), op) |
| 185 | if idx < 0 { |
| 186 | return within |
| 187 | } |
| 188 | return posrange.PositionRange{ |
| 189 | Start: within.Start + posrange.Pos(idx), |
| 190 | End: within.Start + posrange.Pos(idx+len(op)), |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | // FindMatcherPos locates 'name op "value"' (e.g. job=~"foo") in the expression |
| 195 | // fragment and returns the position spanning from "name" through the closing |
| 196 | // quote, with End exclusive (one past the closing quote) to match the other |
| 197 | // position functions and GetQueryFragment. |
| 198 | func FindMatcherPos(expr string, within posrange.PositionRange, m *labels.Matcher) posrange.PositionRange { |
| 199 | fragment := GetQueryFragment(expr, within) |
| 200 | // Build the full pattern: name + op + "value" (e.g. job=~"foo"). |
| 201 | pattern := m.Name + m.Type.String() + `"` + m.Value + `"` |
| 202 | idx := strings.Index(fragment, pattern) |
| 203 | if idx < 0 { |
| 204 | return within |
| 205 | } |
| 206 | return posrange.PositionRange{ |
| 207 | Start: within.Start + posrange.Pos(idx), |
| 208 | End: within.Start + posrange.Pos(idx+len(pattern)), |
| 209 | } |
| 210 | } |
| 211 | |