cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
5bfeffcd0e666ca2da0b27742d74ea360907f2fc

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/parser/utils/source.go

1522lines · modecode

1package utils
2
3import (
4 "fmt"
5 "math"
6 "regexp"
7 "slices"
8 "strconv"
9 "strings"
10
11 "github.com/prometheus/prometheus/model/labels"
12 promParser "github.com/prometheus/prometheus/promql/parser"
13 "github.com/prometheus/prometheus/promql/parser/posrange"
14)
15
16var guaranteedLabelsMatches = []labels.MatchType{labels.MatchEqual, labels.MatchRegexp}
17
18type SourceType uint8
19
20const (
21 UnknownSource SourceType = iota
22 NumberSource
23 StringSource
24 SelectorSource
25 FuncSource
26 AggregateSource
27)
28
29// Used for test snapshots.
30func (st SourceType) MarshalYAML() (any, error) {
31 var name string
32 switch st { // nolint: exhaustive
33 case NumberSource:
34 name = "number"
35 case StringSource:
36 name = "string"
37 case SelectorSource:
38 name = "selector"
39 case FuncSource:
40 name = "function"
41 case AggregateSource:
42 name = "aggregation"
43 }
44 return name, nil
45}
46
47type LabelPromiseType uint8
48
49const (
50 ImpossibleLabel LabelPromiseType = iota
51 PossibleLabel
52 GuaranteedLabel
53)
54
55// Used for test snapshots.
56func (lpt LabelPromiseType) MarshalYAML() (any, error) {
57 var name string
58 switch lpt {
59 case ImpossibleLabel:
60 name = "excluded"
61 case PossibleLabel:
62 name = "included"
63 case GuaranteedLabel:
64 name = "guaranteed"
65 }
66 return name, nil
67}
68
69type LabelTransform struct {
70 Reason string
71 Kind LabelPromiseType
72 Fragment posrange.PositionRange
73}
74
75type DeadInfo struct {
76 Reason string
77 Fragment posrange.PositionRange
78}
79
80type DeadLabelKind uint8
81
82const (
83 ImpossibleDeadLabel DeadLabelKind = iota
84 OrphanedLabel
85 DuplicatedJoin
86 UnusedLabel
87)
88
89func (dlk DeadLabelKind) String() string {
90 switch dlk {
91 case ImpossibleDeadLabel:
92 return "impossible label"
93 case OrphanedLabel:
94 return "orphaned label"
95 case DuplicatedJoin:
96 return "redundant label"
97 case UnusedLabel:
98 return "unused label"
99 }
100 return "unknown"
101}
102
103type DeadLabel struct {
104 Name string
105 Reason string
106 LabelReason string
107 UsageFragment posrange.PositionRange
108 LabelFragment posrange.PositionRange
109 Kind DeadLabelKind
110}
111
112type ReturnInfo struct {
113 LogicalExpr string
114 ValuePosition posrange.PositionRange
115 ReturnedNumber float64 // If AlwaysReturns=true this is the number that's returned
116 AlwaysReturns bool // True if this source always returns results.
117 KnownReturn bool // True if we always know the return value.
118 IsReturnBool bool // True if this source uses the 'bool' modifier.
119}
120
121type SourceOperation struct {
122 Node promParser.Node
123 Operation string
124 Arguments []string
125}
126
127// Used for test snapshots.
128func (so SourceOperation) MarshalYAML() (any, error) {
129 y := map[string]any{
130 "op": so.Operation,
131 "node": fmt.Sprintf("[%T] %s", so.Node, so.Node.String()),
132 }
133 if so.Arguments != nil {
134 y["args"] = so.Arguments
135 }
136 return y, nil
137}
138
139type SourceOperations []SourceOperation
140
141func MostOuterOperation[T promParser.Node](s Source) (T, bool) {
142 for i := len(s.Operations) - 1; i >= 0; i-- {
143 op := s.Operations[i]
144 if o, ok := op.Node.(T); ok {
145 return o, true
146 }
147 }
148 return *new(T), false
149}
150
151type Join struct {
152 MatchingLabels []string
153 AddedLabels []string
154 Src Source // The source we're joining with.
155 Op promParser.ItemType // The binary operation used for this join.
156 Depth int // Zero if this is a direct join, non-zero otherwise. sum(foo * bar) would be in-direct join.
157 IsOn bool
158}
159
160type Unless struct {
161 MatchingLabels []string
162 Src Source
163 IsOn bool
164}
165
166type Source struct {
167 Labels map[string]LabelTransform
168 DeadInfo *DeadInfo
169 DeadLabels []DeadLabel
170 Returns promParser.ValueType
171 Operations SourceOperations
172 Joins []Join // Any other sources this source joins with.
173 Unless []Unless // Any other sources this source is suppressed by.
174 ReturnInfo ReturnInfo
175 Position posrange.PositionRange
176 Type SourceType
177 FixedLabels bool // Labels are fixed and only allowed labels can be present.
178 IsConditional bool // True if this source is guarded by 'foo > 5' or other condition.
179}
180
181func (s Source) Operation() string {
182 if len(s.Operations) > 0 {
183 return s.Operations[len(s.Operations)-1].Operation
184 }
185 return ""
186}
187
188func (s Source) CanHaveLabel(name string) bool {
189 if v, ok := s.Labels[name]; ok {
190 if v.Kind == ImpossibleLabel {
191 return false
192 }
193 if v.Kind == PossibleLabel || v.Kind == GuaranteedLabel {
194 return true
195 }
196 }
197 return !s.FixedLabels
198}
199
200func (s Source) TransformedLabels(kinds ...LabelPromiseType) []string {
201 names := make([]string, 0, len(s.Labels))
202 for name, l := range s.Labels {
203 if slices.Contains(kinds, l.Kind) {
204 names = append(names, name)
205 }
206 }
207 return names
208}
209
210func (s Source) LabelExcludeReason(name string) (string, posrange.PositionRange) {
211 if l, ok := s.Labels[name]; ok && l.Kind == ImpossibleLabel {
212 return l.Reason, l.Fragment
213 }
214 return s.Labels[""].Reason, s.Labels[""].Fragment
215}
216
217func (s *Source) excludeAllLabels(expr, reason string, fragment, allFragment posrange.PositionRange, except []string) {
218 // Everything that was included until now but will be removed needs an explicit stamp to mark it as gone.
219 for name, l := range s.Labels {
220 if slices.Contains(except, name) {
221 continue
222 }
223 if l.Kind == PossibleLabel || l.Kind == GuaranteedLabel {
224 s.Labels[name] = LabelTransform{
225 Kind: ImpossibleLabel,
226 Reason: reason,
227 Fragment: fragment,
228 }
229 }
230 }
231 // Mark except labels as possible, unless they are already guaranteed.
232 for _, name := range except {
233 if l, ok := s.Labels[name]; ok && l.Kind == GuaranteedLabel {
234 continue
235 }
236
237 // We have grouping labels set, if they are possible mark them as such, if not mark as impossible.
238 if s.CanHaveLabel(name) {
239 s.Labels[name] = LabelTransform{
240 Kind: PossibleLabel,
241 Reason: reason,
242 Fragment: FindArgumentPosition(expr, fragment, name),
243 }
244 } else {
245 r, f := s.LabelExcludeReason(name)
246 s.Labels[name] = LabelTransform{
247 Kind: ImpossibleLabel,
248 Reason: r,
249 Fragment: f,
250 }
251 }
252
253 }
254 s.Labels[""] = LabelTransform{
255 Kind: ImpossibleLabel,
256 Reason: reason,
257 Fragment: allFragment,
258 }
259 s.FixedLabels = true
260}
261
262func (s *Source) excludeLabel(reason string, fragment posrange.PositionRange, name string) {
263 s.Labels[name] = LabelTransform{
264 Kind: ImpossibleLabel,
265 Reason: reason,
266 Fragment: fragment,
267 }
268}
269
270func (s *Source) joinLabels(expr string, within posrange.PositionRange, op promParser.ItemType, names []string, outside []posrange.PositionRange) {
271 for _, name := range names {
272 if l, ok := s.Labels[name]; ok && l.Kind == GuaranteedLabel {
273 s.DeadLabels = append(s.DeadLabels, DeadLabel{
274 Kind: DuplicatedJoin,
275 Name: name,
276 Reason: "Query is trying to join the `" + name + "` label that is already present on the other side of the query.",
277 UsageFragment: FindArgumentPosition(
278 expr,
279 FindFuncPosition(expr, within, promParser.ItemTypeStr[op], outside),
280 name,
281 ),
282 LabelReason: l.Reason,
283 LabelFragment: l.Fragment,
284 })
285 return
286 }
287 s.Labels[name] = LabelTransform{
288 Kind: PossibleLabel,
289 Reason: fmt.Sprintf(
290 "Query is using `%s(%s)`, all labels included inside `%s(...)` will be joined to the results on the other side of the query.",
291 promParser.ItemTypeStr[op], strings.Join(names, ", "), promParser.ItemTypeStr[op],
292 ),
293 Fragment: FindArgumentPosition(
294 expr,
295 FindFuncPosition(expr, within, promParser.ItemTypeStr[op], outside),
296 name,
297 ),
298 }
299 }
300}
301
302func (s *Source) includeLabel(expr, reason string, fragment posrange.PositionRange, name string) {
303 if l, ok := s.Labels[name]; ok && l.Kind == GuaranteedLabel {
304 return
305 }
306 s.Labels[name] = LabelTransform{
307 Kind: PossibleLabel,
308 Reason: reason,
309 Fragment: FindArgumentPosition(expr, fragment, name),
310 }
311}
312
313func (s *Source) guaranteeLabel(reason string, fragment posrange.PositionRange, names ...string) {
314 for _, name := range names {
315 s.Labels[name] = LabelTransform{
316 Kind: GuaranteedLabel,
317 Reason: reason,
318 Fragment: fragment,
319 }
320 }
321}
322
323func (s *Source) checkIncludedLabels(expr string, pos posrange.PositionRange, names []string) {
324 for _, name := range names {
325 if !s.CanHaveLabel(name) {
326 reason, fragment := s.LabelExcludeReason(name)
327 s.DeadLabels = append(s.DeadLabels, DeadLabel{
328 Kind: ImpossibleDeadLabel,
329 Name: name,
330 Reason: "You can't use `" + name + "` because this label is not possible here.",
331 UsageFragment: FindArgumentPosition(expr, pos, name),
332 LabelReason: reason,
333 LabelFragment: fragment,
334 })
335 }
336 }
337}
338
339func (s *Source) hasJoinUsingLabel(name string) bool {
340 for _, j := range s.Joins {
341 if j.IsOn && slices.Contains(j.MatchingLabels, name) {
342 return true
343 }
344 if !j.IsOn && !slices.Contains(j.MatchingLabels, name) {
345 return true
346 }
347 }
348 return false
349}
350
351func (s *Source) checkAggregationLabels(expr string, n *promParser.AggregateExpr) {
352 var pos posrange.PositionRange
353 switch {
354 case len(n.Grouping) == 0:
355 pos = FindFuncNamePosition(expr, n.PosRange, promParser.ItemTypeStr[n.Op])
356 case n.Without:
357 pos = FindFuncPosition(expr, n.PosRange, promParser.ItemTypeStr[promParser.WITHOUT], nil)
358 default:
359 pos = FindFuncPosition(expr, n.PosRange, promParser.ItemTypeStr[promParser.BY], nil)
360 }
361
362 for _, j := range s.Joins {
363 for _, name := range j.AddedLabels {
364 if s.hasJoinUsingLabel(name) {
365 // This label is used for some other join for our source.
366 continue
367 }
368 if !n.Without && slices.Contains(n.Grouping, name) {
369 continue
370 }
371 if n.Without && !slices.Contains(n.Grouping, name) {
372 continue
373 }
374 var (
375 labelPos = pos
376 labelReason string
377 )
378 if t := s.findLabelTransform(name); t != nil {
379 labelPos = t.Fragment
380 labelReason = t.Reason
381 }
382 s.DeadLabels = append(s.DeadLabels, DeadLabel{
383 Kind: UnusedLabel,
384 Name: name,
385 Reason: fmt.Sprintf("Previously joined label `%s` is being removed from the results.", name),
386 UsageFragment: FindArgumentPosition(expr, pos, name),
387 LabelReason: labelReason,
388 LabelFragment: labelPos,
389 })
390 }
391 }
392}
393
394func (s *Source) findLabelTransform(name string) *LabelTransform {
395 if t, ok := s.Labels[name]; ok {
396 return &t
397 }
398 for _, j := range s.Joins {
399 if t := j.Src.findLabelTransform(name); t != nil {
400 return t
401 }
402 }
403 return nil
404}
405
406func (s *Source) checkJoinedLabels(expr string, n *promParser.BinaryExpr, dst Source) (dead []DeadLabel) {
407 pos := findBinOpsOperatorPosition(expr, n, promParser.ItemTypeStr[n.Op])
408 for _, j := range s.Joins {
409 for _, name := range j.AddedLabels {
410 if slices.Contains(n.VectorMatching.Include, name) {
411 // label is included in group_left or group_right
412 continue
413 }
414 if n.VectorMatching.On && slices.Contains(n.VectorMatching.MatchingLabels, name) {
415 // label is included inside on(...)
416 continue
417 }
418 if !n.VectorMatching.On && !slices.Contains(n.VectorMatching.MatchingLabels, name) {
419 // label is NOT included inside ignoring(...)
420 continue
421 }
422 var (
423 labelPos = pos
424 labelReason string
425 )
426 if t := dst.findLabelTransform(name); t != nil {
427 labelPos = t.Fragment
428 labelReason = t.Reason
429 }
430 dead = append(dead, DeadLabel{
431 Kind: OrphanedLabel,
432 Name: name,
433 Reason: fmt.Sprintf("This binary operation prevents previously joined label `%s` from being added to the results.", name),
434 UsageFragment: pos,
435 LabelReason: labelReason,
436 LabelFragment: labelPos,
437 })
438 }
439 }
440 return dead
441}
442
443type Visitor func(s Source, j *Join, u *Unless)
444
445func innerWalk(fn Visitor, s Source, j *Join, u *Unless) {
446 fn(s, j, u)
447 for _, j := range s.Joins {
448 innerWalk(fn, j.Src, &j, nil)
449 }
450 for _, u := range s.Unless {
451 innerWalk(fn, u.Src, nil, &u)
452 }
453}
454
455func (s Source) WalkSources(fn Visitor) {
456 innerWalk(fn, s, nil, nil)
457}
458
459func LabelsSource(expr string, node promParser.Node) (src []Source) {
460 return walkNode(expr, node)
461}
462
463func walkNode(expr string, node promParser.Node) (src []Source) {
464 switch n := node.(type) {
465 case *promParser.AggregateExpr:
466 src = append(src, walkAggregation(expr, n)...)
467
468 case *promParser.BinaryExpr:
469 src = append(src, parseBinOps(expr, n)...)
470
471 case *promParser.Call:
472 src = append(src, parseCall(expr, n)...)
473
474 case *promParser.MatrixSelector:
475 for _, s := range walkNode(expr, n.VectorSelector) {
476 s.Returns = promParser.ValueTypeMatrix
477 /*
478 // Prepend Matrix operation
479 s.Operations = append(SourceOperations{
480 {
481 Operation: "",
482 Node: node,
483 Arguments: nil,
484 },
485 }, s.Operations...)
486 */
487 src = append(src, s)
488 }
489
490 case *promParser.SubqueryExpr:
491 src = append(src, walkNode(expr, n.Expr)...)
492
493 case *promParser.NumberLiteral:
494 var s Source
495 s.Labels = map[string]LabelTransform{}
496 s.Type = NumberSource
497 s.Returns = promParser.ValueTypeScalar
498 s.ReturnInfo.KnownReturn = true
499 s.ReturnInfo.ReturnedNumber = n.Val
500 s.ReturnInfo.AlwaysReturns = true
501 s.ReturnInfo.ValuePosition = n.PosRange
502 s.excludeAllLabels(expr, "This query returns a number value with no labels.", n.PosRange, n.PosRange, nil)
503 s.Position = n.PosRange
504 src = append(src, s)
505
506 case *promParser.ParenExpr:
507 src = append(src, walkNode(expr, n.Expr)...)
508
509 case *promParser.StringLiteral:
510 var s Source
511 s.Labels = map[string]LabelTransform{}
512 s.Type = StringSource
513 s.Returns = promParser.ValueTypeString
514 s.ReturnInfo.AlwaysReturns = true
515 s.excludeAllLabels(expr, "This query returns a string value with no labels.", n.PosRange, n.PosRange, nil)
516 s.Position = n.PosRange
517 src = append(src, s)
518
519 case *promParser.UnaryExpr:
520 src = append(src, walkNode(expr, n.Expr)...)
521
522 case *promParser.StepInvariantExpr:
523 // Not possible to get this from the parser.
524
525 case *promParser.VectorSelector:
526 var s Source
527 s.Labels = map[string]LabelTransform{}
528 s.Type = SelectorSource
529 s.Returns = promParser.ValueTypeVector
530 s.Operations = append(s.Operations, SourceOperation{
531 Operation: "",
532 Node: n,
533 Arguments: nil,
534 })
535 s.guaranteeLabel(
536 "Query will only return series where these labels are present.",
537 n.PosRange,
538 labelsFromSelectors(guaranteedLabelsMatches, n)...,
539 )
540 for _, name := range labelsWithEmptyValueSelector(n) {
541 s.excludeLabel(
542 "Query uses `{"+name+"=\"\"}` selector which will filter out any time series with the `"+name+"` label set.",
543 n.PosRange,
544 name,
545 )
546 }
547 s.Position = n.PosRange
548 src = append(src, s)
549
550 default:
551 // unhandled type
552 }
553 return src
554}
555
556func appendToSlice(dst []string, values ...string) []string {
557 for _, v := range values {
558 if !slices.Contains(dst, v) {
559 dst = append(dst, v)
560 }
561 }
562 return dst
563}
564
565func labelsFromSelectors(matches []labels.MatchType, selector *promParser.VectorSelector) (names []string) {
566 if selector == nil {
567 return nil
568 }
569 // Any label used in positive filters is gurnateed to be present.
570 for _, lm := range selector.LabelMatchers {
571 if lm.Name == labels.MetricName {
572 continue
573 }
574 if !slices.Contains(matches, lm.Type) {
575 continue
576 }
577 names = appendToSlice(names, lm.Name)
578 }
579 return names
580}
581
582func labelsWithEmptyValueSelector(selector *promParser.VectorSelector) (names []string) {
583 for _, lm := range selector.LabelMatchers {
584 if lm.Name == labels.MetricName {
585 continue
586 }
587 if lm.Type == labels.MatchEqual && lm.Value == "" {
588 names = appendToSlice(names, lm.Name)
589 }
590 }
591 return names
592}
593
594func GetQueryFragment(expr string, pos posrange.PositionRange) string {
595 return expr[pos.Start:pos.End]
596}
597
598func walkAggregation(expr string, n *promParser.AggregateExpr) (src []Source) {
599 var s Source
600
601 var args []string
602 if n.Param != nil {
603 args = append(args, n.Param.String())
604 }
605
606 switch n.Op {
607 case promParser.COUNT_VALUES:
608 for _, s = range parseAggregation(expr, n) {
609 s.Operations = append(s.Operations, SourceOperation{
610 Operation: promParser.ItemTypeStr[n.Op],
611 Node: n,
612 Arguments: args,
613 })
614 // Param is the label to store the count value in.
615 s.guaranteeLabel(
616 "This label will be added to the results by the count_values() call.",
617 n.PosRange,
618 n.Param.(*promParser.StringLiteral).Val,
619 )
620 if n.Without || !slices.Contains(n.Grouping, labels.MetricName) {
621 s.excludeLabel("Aggregation removes metric name.", n.PosRange, labels.MetricName)
622 }
623 src = append(src, s)
624 }
625 case promParser.TOPK, promParser.BOTTOMK:
626 for _, s = range walkNode(expr, n.Expr) {
627 for i := range s.Joins {
628 s.Joins[i].Depth++
629 }
630 s.Type = AggregateSource
631 s.Operations = append(s.Operations, SourceOperation{
632 Operation: promParser.ItemTypeStr[n.Op],
633 Node: n,
634 Arguments: args,
635 })
636 src = append(src, s)
637 }
638 default:
639 for _, s = range parseAggregation(expr, n) {
640 s.Operations = append(s.Operations, SourceOperation{
641 Operation: promParser.ItemTypeStr[n.Op],
642 Node: n,
643 Arguments: args,
644 })
645 if n.Without || !slices.Contains(n.Grouping, labels.MetricName) {
646 s.excludeLabel("Aggregation removes metric name.", n.PosRange, labels.MetricName)
647 }
648 src = append(src, s)
649 }
650 /*
651 TODO these are experimental and promParser.EnableExperimentalFunctions must be set to true to enable parsing of these.
652 case promParser.LIMITK:
653 s = walkNode(expr, n.Expr)
654 s.Type = AggregateSource
655 s.Operation = "limitk"
656 case promParser.LIMIT_RATIO:
657 s = walkNode(expr, n.Expr)
658 s.Type = AggregateSource
659 s.Operation = "limit_ratio"
660 */
661 }
662 return src
663}
664
665func parseAggregation(expr string, n *promParser.AggregateExpr) (src []Source) {
666 var s Source
667 for _, s = range walkNode(expr, n.Expr) {
668 // If we have sum(foo * bar) then we start with:
669 // - source: foo
670 // joins: bar
671 // Then we parse aggregation and end up with:
672 // - source: sum(foo)
673 // joins: bar
674 // Which is misleading and wrong, so we bump depth value to know about it.
675 for i := range s.Joins {
676 s.Joins[i].Depth++
677 }
678
679 s.checkAggregationLabels(expr, n)
680 if n.Without {
681 for _, name := range n.Grouping {
682 s.excludeLabel(
683 fmt.Sprintf("Query is using aggregation with `%s(%s)`, all labels included inside `%s(...)` will be removed from the results.",
684 promParser.ItemTypeStr[promParser.WITHOUT], strings.Join(n.Grouping, ", "), promParser.ItemTypeStr[promParser.WITHOUT]),
685 FindArgumentPosition(
686 expr,
687 FindFuncPosition(expr, n.PosRange, promParser.ItemTypeStr[promParser.WITHOUT], nil),
688 name,
689 ),
690 name,
691 )
692 }
693 } else {
694 if len(n.Grouping) == 0 {
695 funcNamePos := FindFuncNamePosition(expr, n.PosRange, promParser.ItemTypeStr[n.Op])
696 s.excludeAllLabels(
697 expr,
698 "Query is using aggregation that removes all labels.",
699 funcNamePos,
700 funcNamePos,
701 nil,
702 )
703 } else {
704 s.checkIncludedLabels(
705 expr,
706 FindFuncPosition(expr, n.PosRange, promParser.ItemTypeStr[promParser.BY], nil),
707 n.Grouping,
708 )
709 s.excludeAllLabels(
710 expr,
711 fmt.Sprintf("Query is using aggregation with `%s(%s)`, only labels included inside `%s(...)` will be present on the results.",
712 promParser.ItemTypeStr[promParser.BY], strings.Join(n.Grouping, ", "), promParser.ItemTypeStr[promParser.BY]),
713 FindFuncPosition(expr, n.PosRange, promParser.ItemTypeStr[promParser.BY], nil),
714 FindFuncNamePosition(expr, n.PosRange, promParser.ItemTypeStr[promParser.BY]),
715 n.Grouping,
716 )
717 }
718 }
719 s.Type = AggregateSource
720 s.Returns = promParser.ValueTypeVector
721 src = append(src, s)
722 }
723 return src
724}
725
726func parsePromQLFunc(s Source, expr string, n *promParser.Call) Source {
727 switch n.Func.Name {
728 case "abs", "sgn", "acos", "acosh", "asin", "asinh", "atan", "atanh", "cos", "cosh", "sin", "sinh", "tan", "tanh":
729 // No change to labels.
730 s.Returns = promParser.ValueTypeVector
731 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
732 s.guaranteeLabel(
733 "Query will only return series where these labels are present.",
734 n.PosRange,
735 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
736 )
737
738 case "ceil", "floor", "round":
739 // No change to labels.
740 s.Returns = promParser.ValueTypeVector
741 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
742 s.guaranteeLabel(
743 "Query will only return series where these labels are present.",
744 n.PosRange,
745 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
746 )
747
748 case "changes", "resets":
749 // No change to labels.
750 s.Returns = promParser.ValueTypeVector
751 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
752 s.guaranteeLabel(
753 "Query will only return series where these labels are present.",
754 n.PosRange,
755 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
756 )
757
758 case "clamp", "clamp_max", "clamp_min":
759 // No change to labels.
760 s.Returns = promParser.ValueTypeVector
761 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
762 s.guaranteeLabel(
763 "Query will only return series where these labels are present.",
764 n.PosRange,
765 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
766 )
767
768 case "absent", "absent_over_time":
769 s.Returns = promParser.ValueTypeVector
770 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
771 names := labelsFromSelectors([]labels.MatchType{labels.MatchEqual}, vs)
772 funcNamePos := FindFuncNamePosition(expr, n.PosRange, n.Func.Name)
773 s.excludeAllLabels(
774 expr,
775 fmt.Sprintf(`The [%s()](https://prometheus.io/docs/prometheus/latest/querying/functions/#%s) function is used to check if provided query doesn't match any time series.
776You will only get any results back if the metric selector you pass doesn't match anything.
777Since there are no matching time series there are also no labels. If some time series is missing you cannot read its labels.
778This means that the only labels you can get back from absent call are the ones you pass to it.
779If you're hoping to get instance specific labels this way and alert when some target is down then that won't work, use the `+"`up`"+` metric instead.`,
780 n.Func.Name, n.Func.Name),
781 funcNamePos,
782 funcNamePos,
783 names,
784 )
785 s.guaranteeLabel(
786 fmt.Sprintf("All labels passed to %s() call will be present on the results if the query doesn't match anything.", n.Func.Name),
787 n.PosRange,
788 names...,
789 )
790
791 case "avg_over_time", "count_over_time", "last_over_time", "max_over_time", "min_over_time", "present_over_time", "quantile_over_time", "stddev_over_time", "stdvar_over_time", "sum_over_time":
792 // No change to labels.
793 s.Returns = promParser.ValueTypeVector
794 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
795 s.guaranteeLabel(
796 "Query will only return series where these labels are present.",
797 n.PosRange,
798 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
799 )
800
801 case "days_in_month", "day_of_month", "day_of_week", "day_of_year", "hour", "minute", "month", "year":
802 s.Returns = promParser.ValueTypeVector
803 // No labels if we don't pass any arguments.
804 // Otherwise no change to labels.
805 if len(n.Args) == 0 {
806 s.ReturnInfo.AlwaysReturns = true
807 s.excludeAllLabels(
808 expr,
809 fmt.Sprintf("Calling `%s()` with no arguments will return an empty time series with no labels.",
810 n.Func.Name),
811 n.PosRange,
812 n.PosRange,
813 nil,
814 )
815 } else {
816 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
817 s.guaranteeLabel(
818 "Query will only return series where these labels are present.",
819 n.PosRange,
820 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
821 )
822 }
823
824 case "deg", "rad", "ln", "log10", "log2", "sqrt", "exp":
825 // No change to labels.
826 s.Returns = promParser.ValueTypeVector
827 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
828 s.guaranteeLabel(
829 "Query will only return series where these labels are present.",
830 n.PosRange,
831 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
832 )
833 case "delta", "idelta", "increase", "deriv", "irate", "rate":
834 // No change to labels.
835 s.Returns = promParser.ValueTypeVector
836 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
837 s.guaranteeLabel(
838 "Query will only return series where these labels are present.",
839 n.PosRange,
840 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
841 )
842
843 case "histogram_avg", "histogram_count", "histogram_sum", "histogram_stddev", "histogram_stdvar", "histogram_fraction", "histogram_quantile":
844 // No change to labels.
845 s.Returns = promParser.ValueTypeVector
846 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
847 s.guaranteeLabel(
848 "Query will only return series where these labels are present.",
849 n.PosRange,
850 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
851 )
852
853 case "holt_winters", "predict_linear":
854 // No change to labels.
855 s.Returns = promParser.ValueTypeVector
856 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
857 s.guaranteeLabel(
858 "Query will only return series where these labels are present.",
859 n.PosRange,
860 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
861 )
862 case "label_replace", "label_join":
863 // One label added to the results.
864 s.Returns = promParser.ValueTypeVector
865 s.guaranteeLabel(
866 fmt.Sprintf("This label will be added to the result by %s() call.", n.Func.Name),
867 n.PosRange,
868 n.Args[1].(*promParser.StringLiteral).Val,
869 )
870
871 case "pi":
872 s.Returns = promParser.ValueTypeScalar
873 s.ReturnInfo.AlwaysReturns = true
874 s.ReturnInfo.KnownReturn = true
875 s.ReturnInfo.ReturnedNumber = math.Pi
876 s.ReturnInfo.ValuePosition = n.PosRange
877 s.excludeAllLabels(
878 expr,
879 fmt.Sprintf("Calling `%s()` will return a scalar value with no labels.", n.Func.Name),
880 n.PosRange,
881 n.PosRange,
882 nil,
883 )
884
885 case "scalar":
886 s.Returns = promParser.ValueTypeScalar
887 s.ReturnInfo.AlwaysReturns = true
888 funcPos := FindFuncPosition(expr, n.PositionRange(), n.Func.Name, nil)
889 s.excludeAllLabels(
890 expr,
891 fmt.Sprintf("Calling `%s()` will return a scalar value with no labels.", n.Func.Name),
892 funcPos,
893 funcPos,
894 nil,
895 )
896
897 case "sort", "sort_desc":
898 // No change to labels.
899 s.Returns = promParser.ValueTypeVector
900
901 case "time":
902 s.Returns = promParser.ValueTypeScalar
903 s.ReturnInfo.AlwaysReturns = true
904 s.ReturnInfo.ValuePosition = n.PosRange
905 s.excludeAllLabels(
906 expr,
907 fmt.Sprintf("Calling `%s()` will return a scalar value with no labels.", n.Func.Name),
908 n.PosRange,
909 n.PosRange,
910 nil,
911 )
912
913 case "timestamp":
914 // No change to labels.
915 s.Returns = promParser.ValueTypeVector
916 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
917 s.guaranteeLabel(
918 "Query will only return series where these labels are present.",
919 n.PosRange,
920 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
921 )
922
923 case "vector":
924 s.Returns = promParser.ValueTypeVector
925 s.ReturnInfo.AlwaysReturns = true
926 s.ReturnInfo.ValuePosition = n.PosRange
927 for _, vs := range walkNode(expr, n.Args[0]) {
928 if vs.ReturnInfo.KnownReturn {
929 s.ReturnInfo.ReturnedNumber = vs.ReturnInfo.ReturnedNumber
930 s.ReturnInfo.KnownReturn = true
931 }
932 }
933 funcNamePos := FindFuncNamePosition(expr, n.PosRange, n.Func.Name)
934 s.excludeAllLabels(
935 expr,
936 fmt.Sprintf("Calling `%s()` will return a vector value with no labels.", n.Func.Name),
937 funcNamePos,
938 funcNamePos,
939 nil,
940 )
941
942 default:
943 // Unsupported function
944 return Source{} // nolint: exhaustruct
945 }
946 return s
947}
948
949func parseCall(expr string, n *promParser.Call) (src []Source) {
950 var args []string
951 var exprs []promParser.Expr
952
953 var vt promParser.ValueType
954 for i, e := range n.Args {
955 if i >= len(n.Func.ArgTypes) {
956 vt = n.Func.ArgTypes[len(n.Func.ArgTypes)-1]
957 } else {
958 vt = n.Func.ArgTypes[i]
959 }
960
961 switch vt {
962 case promParser.ValueTypeVector, promParser.ValueTypeMatrix:
963 exprs = append(exprs, e)
964 case promParser.ValueTypeNone, promParser.ValueTypeScalar, promParser.ValueTypeString:
965 args = append(args, e.String())
966 }
967 }
968
969 for _, e := range exprs {
970 for _, es := range walkNode(expr, e) {
971 es.Type = FuncSource
972 es.Operations = append(es.Operations, SourceOperation{
973 Operation: n.Func.Name,
974 Node: n,
975 Arguments: args,
976 })
977 es.Position = e.PositionRange()
978 src = append(src, parsePromQLFunc(es, expr, n))
979 }
980 }
981
982 if len(src) == 0 {
983 s := Source{ // nolint: exhaustruct
984 Labels: map[string]LabelTransform{},
985 Type: FuncSource,
986 Operations: SourceOperations{
987 {
988 Operation: n.Func.Name,
989 Node: n,
990 Arguments: args,
991 },
992 },
993 Position: n.PosRange,
994 }
995 src = append(src, parsePromQLFunc(s, expr, n))
996 }
997
998 return src
999}
1000
1001func parseBinOps(expr string, n *promParser.BinaryExpr) (src []Source) {
1002 pos := n.PositionRange()
1003 src = make([]Source, 0, 2)
1004 switch {
1005 // foo{} + 1
1006 // 1 + foo{}
1007 // foo{} > 1
1008 // 1 > foo{}
1009 case n.VectorMatching == nil:
1010 lhs := walkNode(expr, n.LHS)
1011 rhs := walkNode(expr, n.RHS)
1012 for _, ls := range lhs {
1013 ls.IsConditional, ls.ReturnInfo.IsReturnBool = checkConditions(ls, n.Op, n.ReturnBool)
1014 for _, rs := range rhs {
1015 rs.IsConditional, rs.ReturnInfo.IsReturnBool = checkConditions(rs, n.Op, n.ReturnBool)
1016 var side Source
1017 switch {
1018 case ls.Returns == promParser.ValueTypeVector, ls.Returns == promParser.ValueTypeMatrix:
1019 // Use labels from LHS
1020 side = ls
1021 case rs.Returns == promParser.ValueTypeVector, rs.Returns == promParser.ValueTypeMatrix:
1022 // Use labels from RHS
1023 side = rs
1024 default:
1025 side = ls
1026 }
1027 if ls.ReturnInfo.AlwaysReturns && rs.ReturnInfo.AlwaysReturns && ls.ReturnInfo.KnownReturn && rs.ReturnInfo.KnownReturn {
1028 // Both sides always return something
1029 side.ReturnInfo, side.DeadInfo = calculateStaticReturn(expr, ls, rs, n)
1030 }
1031 src = append(src, side)
1032 }
1033 }
1034
1035 // foo{} + bar{}
1036 // foo{} + on(...) bar{}
1037 // foo{} * ignoring(...) bar{}
1038 // foo{} / bar{}
1039 case n.VectorMatching.Card == promParser.CardOneToOne:
1040 rhs := walkNode(expr, n.RHS)
1041 for _, ls := range walkNode(expr, n.LHS) {
1042 if n.VectorMatching.On {
1043 ls.checkIncludedLabels(expr, pos, n.VectorMatching.MatchingLabels)
1044 funcPos := FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.ON], []posrange.PositionRange{
1045 n.LHS.PositionRange(), n.RHS.PositionRange(),
1046 })
1047 ls.excludeAllLabels(
1048 expr,
1049 fmt.Sprintf(
1050 "Query is using %s vector matching with `%s(%s)`, only labels included inside `%s(...)` will be present on the results.",
1051 n.VectorMatching.Card, promParser.ItemTypeStr[promParser.ON], strings.Join(n.VectorMatching.MatchingLabels, ", "), promParser.ItemTypeStr[promParser.ON],
1052 ),
1053 funcPos,
1054 funcPos,
1055 n.VectorMatching.MatchingLabels,
1056 )
1057 } else {
1058 for _, name := range n.VectorMatching.MatchingLabels {
1059 ls.excludeLabel(
1060 fmt.Sprintf(
1061 "Query is using %s vector matching with `%s(%s)`, all labels included inside `%s(...)` will be removed on the results.",
1062 n.VectorMatching.Card, promParser.ItemTypeStr[promParser.IGNORING], strings.Join(n.VectorMatching.MatchingLabels, ", "), promParser.ItemTypeStr[promParser.IGNORING],
1063 ),
1064 FindArgumentPosition(
1065 expr,
1066 FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.IGNORING], []posrange.PositionRange{
1067 n.LHS.PositionRange(), n.RHS.PositionRange(),
1068 }),
1069 name,
1070 ),
1071 name,
1072 )
1073 }
1074 for _, rs := range rhs {
1075 rs.IsConditional, rs.ReturnInfo.IsReturnBool = checkConditions(rs, n.Op, n.ReturnBool)
1076 if ls.ReturnInfo.AlwaysReturns && rs.ReturnInfo.AlwaysReturns && ls.ReturnInfo.KnownReturn && rs.ReturnInfo.KnownReturn {
1077 // Both sides always return something
1078 ls.ReturnInfo, ls.DeadInfo = calculateStaticReturn(expr, ls, rs, n)
1079 }
1080 }
1081 }
1082 for _, rs := range rhs {
1083 rs.DeadLabels = append(rs.DeadLabels, rs.checkJoinedLabels(expr, n, rs)...)
1084 if ok, s, p := canJoin(ls, rs, n.VectorMatching); !ok {
1085 rs.DeadInfo = &DeadInfo{
1086 Reason: s,
1087 Fragment: p,
1088 }
1089 }
1090 ls.Joins = append(ls.Joins, Join{
1091 Src: rs,
1092 Op: n.Op,
1093 Depth: 0,
1094 MatchingLabels: n.VectorMatching.MatchingLabels,
1095 AddedLabels: nil,
1096 IsOn: n.VectorMatching.On,
1097 })
1098 }
1099 ls.DeadLabels = append(ls.DeadLabels, ls.checkJoinedLabels(expr, n, ls)...)
1100 ls.excludeLabel("Binary operation between two vectors removes metric names.", pos, labels.MetricName)
1101 ls.IsConditional, ls.ReturnInfo.IsReturnBool = checkConditions(ls, n.Op, n.ReturnBool)
1102 src = append(src, ls)
1103 }
1104
1105 // foo{} + on(...) group_right(...) bar{}
1106 // foo{} + ignoring(...) group_right(...) bar{}
1107 case n.VectorMatching.Card == promParser.CardOneToMany:
1108 lhs := walkNode(expr, n.LHS)
1109 for _, rs := range walkNode(expr, n.RHS) {
1110 rs.joinLabels(expr, pos, promParser.GROUP_RIGHT, n.VectorMatching.Include, []posrange.PositionRange{
1111 n.LHS.PositionRange(), n.RHS.PositionRange(),
1112 })
1113 // If we have:
1114 // foo * on(instance) group_left(a,b) bar{x="y"}
1115 // then only group_left() labels will be included.
1116 if n.VectorMatching.On {
1117 rs.checkIncludedLabels(expr, pos, n.VectorMatching.MatchingLabels)
1118 for _, name := range n.VectorMatching.MatchingLabels {
1119 rs.includeLabel(
1120 expr,
1121 fmt.Sprintf(
1122 "Query is using %s vector matching with `on(%s)`, labels included inside `on(...)` will be present on the results.",
1123 n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "),
1124 ),
1125 FindArgumentPosition(
1126 expr,
1127 FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.ON], []posrange.PositionRange{
1128 n.LHS.PositionRange(), n.RHS.PositionRange(),
1129 }),
1130 name,
1131 ),
1132 name,
1133 )
1134 }
1135 }
1136 for _, ls := range lhs {
1137 ls.checkIncludedLabels(
1138 expr,
1139 FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.GROUP_RIGHT], []posrange.PositionRange{
1140 n.LHS.PositionRange(),
1141 n.RHS.PositionRange(),
1142 }),
1143 n.VectorMatching.Include,
1144 )
1145 rs.DeadLabels = append(rs.DeadLabels, ls.checkJoinedLabels(expr, n, rs)...)
1146 if ok, s, p := canJoin(rs, ls, n.VectorMatching); !ok {
1147 ls.DeadInfo = &DeadInfo{
1148 Reason: s,
1149 Fragment: p,
1150 }
1151 }
1152 rs.Joins = append(rs.Joins, Join{
1153 Src: ls,
1154 Op: n.Op,
1155 Depth: 0,
1156 MatchingLabels: n.VectorMatching.MatchingLabels,
1157 AddedLabels: n.VectorMatching.Include,
1158 IsOn: n.VectorMatching.On,
1159 })
1160 }
1161 rs.excludeLabel("Binary operation between two vectors removes metric names.", pos, labels.MetricName)
1162 rs.IsConditional, rs.ReturnInfo.IsReturnBool = checkConditions(rs, n.Op, n.ReturnBool)
1163 src = append(src, rs)
1164 }
1165
1166 // foo{} + on(...) group_left(...) bar{}
1167 // foo{} + ignoring(...) group_left(...) bar{}
1168 case n.VectorMatching.Card == promParser.CardManyToOne:
1169 rhs := walkNode(expr, n.RHS)
1170 for _, ls := range walkNode(expr, n.LHS) {
1171 ls.joinLabels(expr, pos, promParser.GROUP_LEFT, n.VectorMatching.Include, []posrange.PositionRange{
1172 n.LHS.PositionRange(), n.RHS.PositionRange(),
1173 })
1174 if n.VectorMatching.On {
1175 ls.checkIncludedLabels(expr, pos, n.VectorMatching.MatchingLabels)
1176 for _, name := range n.VectorMatching.MatchingLabels {
1177 ls.includeLabel(
1178 expr,
1179 fmt.Sprintf(
1180 "Query is using %s vector matching with `on(%s)`, labels included inside `on(...)` will be present on the results.",
1181 n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "),
1182 ),
1183 FindArgumentPosition(
1184 expr,
1185 FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.ON], []posrange.PositionRange{
1186 n.LHS.PositionRange(), n.RHS.PositionRange(),
1187 }),
1188 name,
1189 ),
1190 name,
1191 )
1192 }
1193 }
1194 for _, rs := range rhs {
1195 rs.checkIncludedLabels(
1196 expr,
1197 FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.GROUP_LEFT], []posrange.PositionRange{
1198 n.LHS.PositionRange(),
1199 n.RHS.PositionRange(),
1200 }),
1201 n.VectorMatching.Include,
1202 )
1203 ls.DeadLabels = append(ls.DeadLabels, rs.checkJoinedLabels(expr, n, ls)...)
1204 if ok, s, p := canJoin(ls, rs, n.VectorMatching); !ok {
1205 rs.DeadInfo = &DeadInfo{
1206 Reason: s,
1207 Fragment: p,
1208 }
1209 }
1210 ls.Joins = append(ls.Joins, Join{
1211 Src: rs,
1212 Op: n.Op,
1213 Depth: 0,
1214 MatchingLabels: n.VectorMatching.MatchingLabels,
1215 AddedLabels: n.VectorMatching.Include,
1216 IsOn: n.VectorMatching.On,
1217 })
1218 }
1219 ls.excludeLabel("Binary operation between two vectors removes metric names.", pos, labels.MetricName)
1220 ls.IsConditional, ls.ReturnInfo.IsReturnBool = checkConditions(ls, n.Op, n.ReturnBool)
1221 src = append(src, ls)
1222 }
1223
1224 // foo{} and on(...) bar{}
1225 // foo{} and ignoring(...) bar{}
1226 // foo{} and bar{}
1227 // foo{} unless bar{}
1228 case n.VectorMatching.Card == promParser.CardManyToMany:
1229 var lhsCanBeEmpty bool // true if any of the LHS query can produce empty results.
1230 rhs := walkNode(expr, n.RHS)
1231 for _, ls := range walkNode(expr, n.LHS) {
1232 var rhsConditional bool
1233 if n.VectorMatching.On {
1234 ls.checkIncludedLabels(expr, pos, n.VectorMatching.MatchingLabels)
1235 for _, name := range n.VectorMatching.MatchingLabels {
1236 ls.includeLabel(
1237 expr,
1238 fmt.Sprintf(
1239 "Query is using %s vector matching with `on(%s)`, labels included inside `on(...)` will be present on the results.",
1240 n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "),
1241 ),
1242 FindArgumentPosition(
1243 expr,
1244 FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.ON], []posrange.PositionRange{
1245 n.LHS.PositionRange(), n.RHS.PositionRange(),
1246 }),
1247 name,
1248 ),
1249 name,
1250 )
1251 }
1252 }
1253 if !ls.ReturnInfo.AlwaysReturns || ls.IsConditional {
1254 lhsCanBeEmpty = true
1255 }
1256 for _, rs := range rhs {
1257 isConditional, _ := checkConditions(rs, n.Op, n.ReturnBool)
1258 if isConditional {
1259 rhsConditional = true
1260 }
1261 if ok, s, p := canJoin(ls, rs, n.VectorMatching); !ok {
1262 rs.DeadInfo = &DeadInfo{
1263 Reason: s,
1264 Fragment: p,
1265 }
1266 }
1267 switch {
1268 case n.Op == promParser.LUNLESS:
1269 if n.VectorMatching.On && len(n.VectorMatching.MatchingLabels) == 0 && rs.ReturnInfo.AlwaysReturns && !rs.IsConditional {
1270 ls.DeadInfo = &DeadInfo{
1271 Reason: "This query will never return anything because the `unless` query always returns something.",
1272 Fragment: rs.Position,
1273 }
1274 }
1275 ls.Unless = append(ls.Unless, Unless{
1276 Src: rs,
1277 MatchingLabels: n.VectorMatching.MatchingLabels,
1278 IsOn: n.VectorMatching.On,
1279 })
1280 case n.Op != promParser.LOR:
1281 ls.Joins = append(ls.Joins, Join{
1282 Src: rs,
1283 Op: n.Op,
1284 Depth: 0,
1285 MatchingLabels: n.VectorMatching.MatchingLabels,
1286 AddedLabels: nil,
1287 IsOn: n.VectorMatching.On,
1288 })
1289 }
1290 ls.DeadLabels = append(ls.DeadLabels, rs.checkJoinedLabels(expr, n, ls)...)
1291 }
1292 if n.Op == promParser.LAND && rhsConditional {
1293 ls.IsConditional = true
1294 }
1295 src = append(src, ls)
1296 }
1297 if n.Op == promParser.LOR {
1298 for _, rs := range rhs {
1299 // If LHS can NOT be empty then RHS is dead code.
1300 if !lhsCanBeEmpty {
1301 rs.DeadInfo = &DeadInfo{
1302 Reason: "The left hand side always returs something and so the right hand side is never used.",
1303 Fragment: rs.Position,
1304 }
1305 }
1306 src = append(src, rs)
1307 }
1308 }
1309 }
1310 return src
1311}
1312
1313func checkConditions(s Source, op promParser.ItemType, isBool bool) (isConditional, isReturnBool bool) {
1314 if !s.IsConditional && isBool {
1315 isReturnBool = isBool
1316 }
1317 if s.IsConditional {
1318 isConditional = s.IsConditional
1319 } else {
1320 isConditional = op.IsComparisonOperator()
1321 }
1322 return isConditional, isReturnBool
1323}
1324
1325func canJoin(ls, rs Source, vm *promParser.VectorMatching) (bool, string, posrange.PositionRange) {
1326 var side string
1327 if vm.Card == promParser.CardOneToMany {
1328 side = "left"
1329 } else {
1330 side = "right"
1331 }
1332
1333 switch {
1334 case vm.On && len(vm.MatchingLabels) == 0: // ls on() unless rs
1335 return true, "", posrange.PositionRange{}
1336 case vm.On: // ls on(...) unless rs
1337 for _, name := range vm.MatchingLabels {
1338 if ls.CanHaveLabel(name) && !rs.CanHaveLabel(name) {
1339 reason, fragment := rs.LabelExcludeReason(name)
1340 return false, fmt.Sprintf("The %s hand side will never be matched because it doesn't have the `%s` label from `on(...)`. %s",
1341 side, name, reason), fragment
1342 }
1343 }
1344 default: // ls unless rs
1345 for name, l := range ls.Labels {
1346 if l.Kind != GuaranteedLabel {
1347 continue
1348 }
1349 if ls.CanHaveLabel(name) && !rs.CanHaveLabel(name) {
1350 reason, fragment := rs.LabelExcludeReason(name)
1351 return false, fmt.Sprintf("The %s hand side will never be matched because it doesn't have the `%s` label while the left hand side will. %s",
1352 side, name, reason), fragment
1353 }
1354 }
1355 }
1356 return true, "", posrange.PositionRange{}
1357}
1358
1359func describeDeadCode(expr string, ls, rs Source, op *promParser.BinaryExpr, match string) *DeadInfo {
1360 var lse, rse string
1361 if ls.ReturnInfo.LogicalExpr != "" {
1362 lse = ls.ReturnInfo.LogicalExpr
1363 } else {
1364 lse = GetQueryFragment(expr, ls.ReturnInfo.ValuePosition)
1365 }
1366 if rs.ReturnInfo.LogicalExpr != "" {
1367 rse = rs.ReturnInfo.LogicalExpr
1368 } else {
1369 rse = GetQueryFragment(expr, rs.ReturnInfo.ValuePosition)
1370 }
1371
1372 cmpPrefix := fmt.Sprintf("`%s %s %s` always evaluates to", lse, op.Op, rse)
1373
1374 var cmpSuffix string
1375 if op.ReturnBool {
1376 cmpSuffix = "and uses the `bool` modifier which means it will always return 0"
1377 } else {
1378 cmpSuffix = "which is not possible, so it will never return anything."
1379 }
1380 return &DeadInfo{
1381 Reason: fmt.Sprintf(
1382 "%s `%s %s %s` %s",
1383 cmpPrefix,
1384 strconv.FormatFloat(ls.ReturnInfo.ReturnedNumber, 'f', -1, 64),
1385 match,
1386 strconv.FormatFloat(rs.ReturnInfo.ReturnedNumber, 'f', -1, 64),
1387 cmpSuffix,
1388 ),
1389 Fragment: ls.Position,
1390 }
1391}
1392
1393func calculateStaticReturn(expr string, ls, rs Source, op *promParser.BinaryExpr) (ret ReturnInfo, deadinfo *DeadInfo) {
1394 ret = ls.ReturnInfo
1395 switch op.Op {
1396 case promParser.EQLC:
1397 if ls.ReturnInfo.ReturnedNumber != rs.ReturnInfo.ReturnedNumber {
1398 deadinfo = describeDeadCode(expr, ls, rs, op, "==")
1399 }
1400 case promParser.NEQ:
1401 if ls.ReturnInfo.ReturnedNumber == rs.ReturnInfo.ReturnedNumber {
1402 deadinfo = describeDeadCode(expr, ls, rs, op, "!=")
1403 }
1404 case promParser.LTE:
1405 if ls.ReturnInfo.ReturnedNumber > rs.ReturnInfo.ReturnedNumber {
1406 deadinfo = describeDeadCode(expr, ls, rs, op, "<=")
1407 }
1408 case promParser.LSS:
1409 if ls.ReturnInfo.ReturnedNumber >= rs.ReturnInfo.ReturnedNumber {
1410 deadinfo = describeDeadCode(expr, ls, rs, op, "<")
1411 }
1412 case promParser.GTE:
1413 if ls.ReturnInfo.ReturnedNumber < rs.ReturnInfo.ReturnedNumber {
1414 deadinfo = describeDeadCode(expr, ls, rs, op, ">=")
1415 }
1416 case promParser.GTR:
1417 if ls.ReturnInfo.ReturnedNumber <= rs.ReturnInfo.ReturnedNumber {
1418 deadinfo = describeDeadCode(expr, ls, rs, op, ">")
1419 }
1420 case promParser.ADD:
1421 ret.ReturnedNumber = ls.ReturnInfo.ReturnedNumber + rs.ReturnInfo.ReturnedNumber
1422 ret.LogicalExpr = formatDesc(expr, ls, rs, "+")
1423 case promParser.SUB:
1424 ret.ReturnedNumber = ls.ReturnInfo.ReturnedNumber - rs.ReturnInfo.ReturnedNumber
1425 ret.LogicalExpr = formatDesc(expr, ls, rs, "-")
1426 case promParser.MUL:
1427 ret.ReturnedNumber = ls.ReturnInfo.ReturnedNumber * rs.ReturnInfo.ReturnedNumber
1428 ret.LogicalExpr = formatDesc(expr, ls, rs, "*")
1429 case promParser.DIV:
1430 ret.ReturnedNumber = ls.ReturnInfo.ReturnedNumber / rs.ReturnInfo.ReturnedNumber
1431 ret.LogicalExpr = formatDesc(expr, ls, rs, "/")
1432 case promParser.MOD:
1433 ret.ReturnedNumber = math.Mod(ls.ReturnInfo.ReturnedNumber, rs.ReturnInfo.ReturnedNumber)
1434 ret.LogicalExpr = formatDesc(expr, ls, rs, "%")
1435 case promParser.POW:
1436 ret.ReturnedNumber = math.Pow(ls.ReturnInfo.ReturnedNumber, rs.ReturnInfo.ReturnedNumber)
1437 ret.LogicalExpr = formatDesc(expr, ls, rs, "^")
1438 }
1439 return ret, deadinfo
1440}
1441
1442func formatDesc(expr string, ls, rs Source, op string) string {
1443 var lse, rse string
1444 if ls.ReturnInfo.LogicalExpr != "" {
1445 lse = ls.ReturnInfo.LogicalExpr
1446 } else {
1447 lse = GetQueryFragment(expr, ls.ReturnInfo.ValuePosition)
1448 }
1449 if rs.ReturnInfo.LogicalExpr != "" {
1450 rse = rs.ReturnInfo.LogicalExpr
1451 } else {
1452 rse = GetQueryFragment(expr, rs.ReturnInfo.ValuePosition)
1453 }
1454 return lse + " " + op + " " + rse
1455}
1456
1457func isOutside(pos posrange.PositionRange, outside []posrange.PositionRange) bool {
1458 for _, out := range outside {
1459 if pos.Start >= out.Start && pos.End <= out.End {
1460 return false
1461 }
1462 }
1463 return true
1464}
1465
1466func FindFuncNamePosition(expr string, within posrange.PositionRange, fn string) posrange.PositionRange {
1467 re := regexp.MustCompile("(?si)(" + regexp.QuoteMeta(fn) + ")(?:[ \n\t]*?)\\(")
1468 idx := re.FindStringSubmatchIndex(GetQueryFragment(expr, within))
1469 if idx == nil {
1470 return within
1471 }
1472 return posrange.PositionRange{
1473 Start: within.Start + posrange.Pos(idx[0]),
1474 End: within.Start + posrange.Pos(idx[1]-1),
1475 }
1476}
1477
1478func FindFuncPosition(expr string, within posrange.PositionRange, fn string, outside []posrange.PositionRange) posrange.PositionRange {
1479 re := regexp.MustCompile("(?si)(" + regexp.QuoteMeta(fn) + ")(?:[ \n\t]*?)\\((?:.*?)\\)")
1480 idx := re.FindStringSubmatchIndex(GetQueryFragment(expr, within))
1481 if idx == nil {
1482 return within
1483 }
1484 var pos posrange.PositionRange
1485 for chk := range slices.Chunk(idx, 2) {
1486 pos = posrange.PositionRange{
1487 Start: within.Start + posrange.Pos(chk[0]),
1488 End: within.Start + posrange.Pos(chk[1]),
1489 }
1490 if isOutside(pos, outside) {
1491 return pos
1492 }
1493 }
1494 return within
1495}
1496
1497func FindArgumentPosition(expr string, within posrange.PositionRange, name string) posrange.PositionRange {
1498 re := regexp.MustCompile("(?s)\\((?:(.*,?))(?:[ \n\t]*?)(" + regexp.QuoteMeta(name) + ")(?:[ \n\t]*?)(?:(,.*)?)\\)")
1499 idx := re.FindStringSubmatchIndex(GetQueryFragment(expr, within))
1500 if idx == nil {
1501 return within
1502 }
1503 return posrange.PositionRange{
1504 Start: within.Start + posrange.Pos(idx[4]),
1505 End: within.Start + posrange.Pos(idx[5]),
1506 }
1507}
1508
1509func findBinOpsOperatorPosition(expr string, n *promParser.BinaryExpr, op string) posrange.PositionRange {
1510 within := posrange.PositionRange{
1511 Start: n.LHS.PositionRange().End + 1,
1512 End: n.RHS.PositionRange().Start,
1513 }
1514 idx := strings.Index(GetQueryFragment(expr, within), op)
1515 if idx < 0 {
1516 return within
1517 }
1518 return posrange.PositionRange{
1519 Start: within.Start + posrange.Pos(idx),
1520 End: within.Start + posrange.Pos(idx+len(op)),
1521 }
1522}