• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

evolvedbinary / elemental / 982

29 Apr 2025 08:34PM UTC coverage: 56.409% (+0.007%) from 56.402%
982

push

circleci

adamretter
[feature] Improve README.md badges

28451 of 55847 branches covered (50.94%)

Branch coverage included in aggregate %.

77468 of 131924 relevant lines covered (58.72%)

0.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.71
/exist-core/src/main/java/org/exist/xquery/Optimizer.java
1
/*
2
 * Elemental
3
 * Copyright (C) 2024, Evolved Binary Ltd
4
 *
5
 * admin@evolvedbinary.com
6
 * https://www.evolvedbinary.com | https://www.elemental.xyz
7
 *
8
 * Use of this software is governed by the Business Source License 1.1
9
 * included in the LICENSE file and at www.mariadb.com/bsl11.
10
 *
11
 * Change Date: 2028-04-27
12
 *
13
 * On the date above, in accordance with the Business Source License, use
14
 * of this software will be governed by the Apache License, Version 2.0.
15
 *
16
 * Additional Use Grant: Production use of the Licensed Work for a permitted
17
 * purpose. A Permitted Purpose is any purpose other than a Competing Use.
18
 * A Competing Use means making the Software available to others in a commercial
19
 * product or service that: substitutes for the Software; substitutes for any
20
 * other product or service we offer using the Software that exists as of the
21
 * date we make the Software available; or offers the same or substantially
22
 * similar functionality as the Software.
23
 *
24
 * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
25
 *       The original license header is included below.
26
 *
27
 * =====================================================================
28
 *
29
 * eXist-db Open Source Native XML Database
30
 * Copyright (C) 2001 The eXist-db Authors
31
 *
32
 * info@exist-db.org
33
 * http://www.exist-db.org
34
 *
35
 * This library is free software; you can redistribute it and/or
36
 * modify it under the terms of the GNU Lesser General Public
37
 * License as published by the Free Software Foundation; either
38
 * version 2.1 of the License, or (at your option) any later version.
39
 *
40
 * This library is distributed in the hope that it will be useful,
41
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
42
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
43
 * Lesser General Public License for more details.
44
 *
45
 * You should have received a copy of the GNU Lesser General Public
46
 * License along with this library; if not, write to the Free Software
47
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
48
 */
49
package org.exist.xquery;
50

51
import org.exist.storage.DBBroker;
52
import org.exist.xquery.functions.array.ArrayConstructor;
53
import org.exist.xquery.pragmas.Optimize;
54
import org.apache.logging.log4j.LogManager;
55
import org.apache.logging.log4j.Logger;
56
import org.exist.xquery.util.ExpressionDumper;
57

58
import javax.annotation.Nullable;
59
import java.util.*;
60

61
/**
62
 * Analyzes the query and marks optimizable expressions for the query engine.
63
 * This class just searches for potentially optimizable expressions in the query tree and
64
 * encloses those expressions with an (#exist:optimize#) pragma. The real optimization
65
 * work is not done by this class but by the pragma (see {@link org.exist.xquery.pragmas.Optimize}).
66
 * The pragma may also decide that the optimization is not applicable and just execute
67
 * the expression without any optimization.
68
 *
69
 * Currently, the optimizer is disabled by default. To enable it, set attribute enable-query-rewriting
70
 * to yes in conf.xml:
71
 *
72
 *  <xquery enable-java-binding="no" enable-query-rewriting="yes">...
73
 * 
74
 * To enable/disable the optimizer for a single query, use an option:
75
 *
76
 * <pre>declare option exist:optimize "enable=yes|no";</pre>
77
 *
78
 */
79
public class Optimizer extends DefaultExpressionVisitor {
80

81
    private static final Logger LOG = LogManager.getLogger(Optimizer.class);
1✔
82

83
    private final XQueryContext context;
84
    private final List<QueryRewriter> rewriters;
85
    private final FindOptimizable findOptimizable = new FindOptimizable();
1✔
86

87
    private int predicates = 0;
1✔
88

89
    private boolean hasOptimized = false;
1✔
90

91
    public Optimizer(final XQueryContext context) {
1✔
92
        this.context = context;
1✔
93
        final DBBroker broker = context.getBroker();
1✔
94
        this.rewriters = broker != null ? broker.getIndexController().getQueryRewriters(context) : Collections.emptyList();
1✔
95
    }
1✔
96

97
    public boolean hasOptimized() {
98
        return hasOptimized;
1✔
99
    }
100

101
    @Override
102
    public void visitLocationStep(final LocationStep locationStep) {
103
        super.visitLocationStep(locationStep);
1✔
104

105
        // check query rewriters if they want to rewrite the location step
106
        Pragma optimizePragma = null;
1✔
107
        try {  // Keep try-catch out of loop
108
            for (final QueryRewriter rewriter : rewriters) {
1!
109
                optimizePragma = rewriter.rewriteLocationStep(locationStep);
×
110
                if (optimizePragma != null) {
×
111
                    // expression was rewritten: return
112
                    hasOptimized = true;
×
113
                    break;
×
114
                }
115
            }
116
        } catch (final XPathException e) {
1✔
117
            LOG.warn("Exception called while rewriting location step: {}", e.getMessage(), e);
×
118
        }
119

120
        boolean optimize = false;
1✔
121
        // only location steps with predicates can be optimized:
122
        @Nullable final Predicate[] preds = locationStep.getPredicates();
1✔
123
        if (preds != null) {
1✔
124
            // walk through the predicates attached to the current location step.
125
            // try to find a predicate containing an expression which is an instance
126
            // of Optimizable.
127
            for (final Predicate pred : preds) {
1✔
128
                pred.accept(findOptimizable);
1✔
129
                @Nullable final Optimizable[] list = findOptimizable.getOptimizables();
1✔
130
                if (canOptimize(list)) {
1✔
131
                    optimize = true;
1✔
132
                }
133
                findOptimizable.reset();
1✔
134
                if (optimize) {
1✔
135
                    break;
1✔
136
                }
137
            }
138
        }
139

140
        final Expression parent = locationStep.getParentExpression();
1✔
141

142
        if (optimize) {
1✔
143
            // we found at least one Optimizable. Rewrite the whole expression and
144
            // enclose it in an (#exist:optimize#) pragma.
145
            if (!(parent instanceof final RewritableExpression path)) {
1!
146
                if (LOG.isTraceEnabled()) {
×
147
                    LOG.trace("Parent expression of step is not a PathExpr: {}", parent);
×
148
                }
149
                return;
×
150
            }
151

152
            hasOptimized = true;
1✔
153

154
            try {
155
                // Create the pragma
156
                final ExtensionExpression extension = new ExtensionExpression(context);
1✔
157
                if (optimizePragma != null) {
1!
158
                    extension.addPragma(optimizePragma);
×
159
                }
160
                extension.addPragma(new Optimize(extension, context, Optimize.OPTIMIZE_PRAGMA, null, false));
1✔
161
                extension.setExpression(locationStep);
1✔
162
                
163
                // Replace the old expression with the pragma
164
                path.replace(locationStep, extension);
1✔
165

166
                if (LOG.isTraceEnabled()) {
1!
167
                    LOG.trace("Rewritten expression: {}", ExpressionDumper.dump(parent));
×
168
                }
169
            } catch (final XPathException e) {
×
170
                LOG.warn("Failed to optimize expression: {}: {}", locationStep, e.getMessage(), e);
×
171
            }
172
        } else if (optimizePragma != null) {
1!
173
            final ExtensionExpression extension = new ExtensionExpression(context);
×
174
            extension.addPragma(optimizePragma);
×
175
            extension.setExpression(locationStep);
×
176

177
            // Replace the old expression with the pragma
178
            final RewritableExpression path = (RewritableExpression) parent;
×
179
            path.replace(locationStep, extension);
×
180
        }
181
    }
1✔
182

183
    @Override
184
    public void visitFilteredExpr(final FilteredExpression filtered) {
185
        super.visitFilteredExpr(filtered);
1✔
186

187
        // check if filtered expression can be simplified:
188
        // handles expressions like //foo/(baz)[...]
189
        if (filtered.getExpression() instanceof final LocationStep step) {
1✔
190
            // single location step: simplify by directly attaching it to the parent path expression
191
            final Expression parent = filtered.getParent();
1✔
192
            if (parent instanceof final RewritableExpression rewritableParentExpression) {
1!
193
                final List<Predicate> preds = filtered.getPredicates();
1✔
194
                final boolean optimizable = hasOptimizable(preds);
1✔
195
                if (optimizable) {
1✔
196
                    // copy predicates
197
                    for (Predicate pred : preds) {
1✔
198
                        step.addPredicate(pred);
1✔
199
                    }
200
                    rewritableParentExpression.replace(filtered, step);
1✔
201
                    step.setParent(parent);
1✔
202
                    visitLocationStep(step);
1✔
203
                    return;
1✔
204
                }
205
            }
206
        }
207

208
        // check if there are any predicates which could be optimized
209
        final List<Predicate> preds = filtered.getPredicates();
1✔
210
        final boolean optimize = hasOptimizable(preds);
1✔
211
        if (optimize) {
1✔
212
            // we found at least one Optimizable. Rewrite the whole expression and
213
            // enclose it in an (#exist:optimize#) pragma.
214
            final Expression parent = filtered.getParent();
1✔
215
            if (!(parent instanceof final RewritableExpression path)) {
1!
216
                if (LOG.isTraceEnabled()) {
×
217
                    LOG.trace("Parent expression: {} of step does not implement RewritableExpression", parent.getClass().getName());
×
218
                }
219
                return;
×
220
            }
221
            if (LOG.isTraceEnabled()) {
1!
222
                LOG.trace("Rewriting expression: {}", ExpressionDumper.dump(filtered));
×
223
            }
224

225
            hasOptimized = true;
1✔
226

227
            try {
228
                // Create the pragma
229
                final ExtensionExpression extension = new ExtensionExpression(context);
1✔
230
                extension.addPragma(new Optimize(extension, context, Optimize.OPTIMIZE_PRAGMA, null, false));
1✔
231
                extension.setExpression(filtered);
1✔
232
                // Replace the old expression with the pragma
233
                path.replace(filtered, extension);
1✔
234
            } catch (final XPathException e) {
1✔
235
                LOG.warn("Failed to optimize expression: {}: {}", filtered, e.getMessage(), e);
×
236
            }
237
        }
238
    }
1✔
239

240
    private boolean hasOptimizable(final List<Predicate> preds) {
241
        // walk through the predicates attached to the current location step.
242
        // try to find a predicate containing an expression which is an instance
243
        // of Optimizable.
244
        boolean optimizable = false;
1✔
245
        for (final Predicate pred : preds) {
1✔
246
            pred.accept(findOptimizable);
1✔
247
            @Nullable final Optimizable[] list = findOptimizable.getOptimizables();
1✔
248
            if (canOptimize(list)) {
1✔
249
                optimizable = true;
1✔
250
            }
251
            findOptimizable.reset();
1✔
252
            if (optimizable) {
1✔
253
                break;
1✔
254
            }
255
        }
256
        return optimizable;
1✔
257
    }
258

259
    @Override
260
    public void visitAndExpr(final OpAnd and) {
261
        if (predicates > 0) {
1✔
262
            // inside a filter expression, we can often replace a logical and with
263
            // a chain of filters, which can then be further optimized
264
            Expression parent = and.getParent();
1✔
265
            if (!(parent instanceof PathExpr)) {
1✔
266
                if (LOG.isTraceEnabled()) {
1!
267
                    LOG.trace("Parent expression of boolean operator is not a PathExpr: {}", parent);
×
268
                }
269
                return;
1✔
270
            }
271

272
            final PathExpr path;
273
            final Predicate predicate;
274
            if (parent instanceof Predicate) {
1!
275
                predicate = (Predicate) parent;
1✔
276
                path = predicate;
1✔
277
            } else {
1✔
278
                path = (PathExpr) parent;
×
279
                parent = path.getParent();
×
280
                if (!(parent instanceof Predicate) || path.getLength() > 1) {
×
281
                    LOG.debug("Boolean operator is not a top-level expression in the predicate: {}", parent == null ? "?" : parent.getClass().getName());
×
282
                    return;
×
283
                }
284
                predicate = (Predicate) parent;
×
285
            }
286

287
            if (LOG.isTraceEnabled()) {
1!
288
                LOG.trace("Rewriting boolean expression: {}", ExpressionDumper.dump(and));
×
289
            }
290
            hasOptimized = true;
1✔
291
            final LocationStep step = (LocationStep) predicate.getParent();
1✔
292
            final Predicate newPred = new Predicate(context);
1✔
293
            newPred.add(simplifyPath(and.getRight()));
1✔
294
            step.insertPredicate(predicate, newPred);
1✔
295
            path.replace(and, simplifyPath(and.getLeft()));
1✔
296
        } else if (and.isRewritable()) {
1!
297
                and.getLeft().accept(this);
1✔
298
                        and.getRight().accept(this);
1✔
299
        }
300
    }
1✔
301

302
    @Override
303
    public void visitOrExpr(final OpOr or) {
304
            if (or.isRewritable()) {
1✔
305
                or.getLeft().accept(this);
1✔
306
                        or.getRight().accept(this);
1✔
307
        }
308
        }
1✔
309

310
    @Override
311
    public void visitGeneralComparison(final GeneralComparison comparison) {
312
        // Check if the left operand is a path expression ending in a
313
        // text() step. This step is unnecessary and makes it hard
314
        // to further optimize the expression. We thus try to remove
315
        // the extra text() step automatically.
316
        // TODO should insert a pragma instead of removing the step
317
        // we don't know at this point if there's an index to use
318
//        Expression expr = comparison.getLeft();
319
//        if (expr instanceof PathExpr) {
320
//            PathExpr pathExpr = (PathExpr) expr;
321
//            Expression last = pathExpr.getLastExpression();
322
//            if (pathExpr.getLength() > 1 && last instanceof Step && ((Step)last).getTest().getType() == Type.TEXT) {
323
//                pathExpr.remove(last);
324
//            }
325
//        }
326
        comparison.getLeft().accept(this);
1✔
327
        comparison.getRight().accept(this);
1✔
328
    }
1✔
329

330
    @Override
331
    public void visitPredicate(final Predicate predicate) {
332
        ++predicates;
1✔
333
        super.visitPredicate(predicate);
1✔
334
        --predicates;
1✔
335
    }
1✔
336

337
    /**
338
     * Check if a global variable can be inlined, usually if it
339
     * references a literal value or sequence thereof.
340
     *
341
     * @param ref the variable reference
342
     */
343
    @Override
344
    public void visitVariableReference(final VariableReference ref) {
345
        final String ns = ref.getName().getNamespaceURI();
1✔
346
        if (ns != null && ns.length() > 0) {
1!
347

348
            @Nullable final Module[] modules = context.getModules(ns);
1✔
349
            if (modules != null) {
1✔
350
                for (final Module module : modules) {
1✔
351
                    if (module != null && !module.isInternalModule()) {
1!
352
                        final Collection<VariableDeclaration> vars = ((ExternalModule) module).getVariableDeclarations();
1✔
353
                        for (final VariableDeclaration var: vars) {
1✔
354
                            if (var.getName().equals(ref.getName()) && var.getExpression().isPresent()) {
1✔
355
                                var.getExpression().get().accept(this);
1✔
356
                                final Expression expression = simplifyPath(var.getExpression().get());
1✔
357
                                final InlineableVisitor visitor = new InlineableVisitor();
1✔
358
                                expression.accept(visitor);
1✔
359
                                if (visitor.isInlineable()) {
1✔
360
                                    final Expression parent = ref.getParent();
1✔
361
                                    if (parent instanceof final RewritableExpression parentRewritableExpression) {
1✔
362
                                        if (LOG.isDebugEnabled()) {
1!
363
                                            LOG.debug("{} line {}: inlining variable {}", ref.getSource().toString(), ref.getLine(), ref.getName());
×
364
                                        }
365
                                        parentRewritableExpression.replace(ref, expression);
1✔
366
                                    }
367
                                }
368

369
                                return;  // exit function!
1✔
370
                            }
371
                        }
372
                    }
373
                }
374
            }
375
        }
376
    }
1✔
377

378
    private boolean canOptimize(@Nullable final Optimizable[] list) {
379
        if (list == null || list.length == 0) {
1!
380
            return false;
1✔
381
        }
382

383
        for (final Optimizable optimizable : list) {
1✔
384
            final int axis = optimizable.getOptimizeAxis();
1✔
385
            if (!(axis == Constants.CHILD_AXIS || axis == Constants.DESCENDANT_AXIS ||
1✔
386
                    axis == Constants.DESCENDANT_SELF_AXIS || axis == Constants.ATTRIBUTE_AXIS ||
1!
387
                    axis == Constants.DESCENDANT_ATTRIBUTE_AXIS || axis == Constants.SELF_AXIS
1!
388
            )) {
389
                return false;
1✔
390
            }
391
        }
392
        return true;
1✔
393
    }
394

395
    private int reverseAxis(final int axis) {
396
        return switch (axis) {
×
397
            case Constants.CHILD_AXIS -> Constants.PARENT_AXIS;
×
398
            case Constants.DESCENDANT_AXIS -> Constants.ANCESTOR_AXIS;
×
399
            case Constants.DESCENDANT_SELF_AXIS -> Constants.ANCESTOR_SELF_AXIS;
×
400
            default -> Constants.UNKNOWN_AXIS;
×
401
        };
402
    }
403

404
    private Expression simplifyPath(final Expression expression) {
405
        if (!(expression instanceof final PathExpr path)) {
1!
406
            return expression;
×
407
        }
408

409
        if (path.getLength() != 1) {
1!
410
            return path;
×
411
        }
412

413
        return path.getExpression(0);
1✔
414
    }
415

416
    /**
417
     * Try to find an expression object implementing interface Optimizable.
418
     */
419
    public static class FindOptimizable extends BasicExpressionVisitor {
1✔
420
        private @Nullable Optimizable[] optimizables = null;
1✔
421

422
        public @Nullable Optimizable[] getOptimizables() {
423
            return optimizables;
1✔
424
        }
425

426
        @Override
427
        public void visitPathExpr(final PathExpr expression) {
428
            for (int i = 0; i < expression.getLength(); i++) {
1✔
429
                final Expression next = expression.getExpression(i);
1✔
430
                next.accept(this);
1✔
431
            }
432
        }
1✔
433

434
        @Override
435
        public void visitGeneralComparison(final GeneralComparison comparison) {
436
            addOptimizable(comparison);
1✔
437
        }
1✔
438

439
        @Override
440
        public void visitPredicate(final Predicate predicate) {
441
            predicate.getExpression(0).accept(this);
1✔
442
        }
1✔
443

444
        @Override
445
        public void visitBuiltinFunction(final Function function) {
446
            if (function instanceof final Optimizable optimizable) {
1✔
447
                addOptimizable(optimizable);
1✔
448
            }
449
        }
1✔
450

451
        private void addOptimizable(final Optimizable optimizable) {
452
            if (optimizables == null) {
1!
453
                optimizables = new Optimizable[1];
1✔
454
            } else {
1✔
455
                optimizables = Arrays.copyOf(optimizables, optimizables.length + 1);
×
456
            }
457
            optimizables[optimizables.length - 1] = optimizable;
1✔
458
        }
1✔
459

460
        /**
461
         * Reset this visitor for reuse.
462
         *
463
         * Clears the known {@link #optimizables}.
464
         */
465
        public void reset() {
466
            this.optimizables = null;
1✔
467
        }
1✔
468
    }
469

470
    /**
471
     * Traverses an expression subtree to check if it could be inlined.
472
     */
473
    static class InlineableVisitor extends DefaultExpressionVisitor {
1✔
474

475
        private boolean inlineable = true;
1✔
476

477
        public boolean isInlineable() {
478
            return inlineable;
1✔
479
        }
480

481
        @Override
482
        public void visit(final Expression expr) {
483
            if (expr instanceof LiteralValue) {
1✔
484
                return;
1✔
485
            }
486

487
            if (expr instanceof Atomize ||
1!
488
                    expr instanceof DynamicCardinalityCheck ||
1!
489
                    expr instanceof DynamicNameCheck ||
1!
490
                    expr instanceof DynamicTypeCheck ||
1!
491
                    expr instanceof UntypedValueCheck ||
1!
492
                    expr instanceof ConcatExpr ||
1!
493
                    expr instanceof ArrayConstructor) {
1!
494
                expr.accept(this);
×
495
            } else {
×
496
                inlineable = false;
1✔
497
            }
498
        }
1✔
499

500
        @Override
501
        public void visitPathExpr(final PathExpr expr) {
502
            // continue to check for numeric operators and other simple constructs,
503
            // abort for all other path expressions with length > 1
504
            if (expr instanceof OpNumeric ||
1!
505
                    expr instanceof SequenceConstructor ||
1✔
506
                    expr.getLength() == 1) {
1✔
507
                super.visitPathExpr(expr);
1✔
508
            } else {
1✔
509
                inlineable = false;
1✔
510
            }
511
        }
1✔
512

513
        @Override
514
        public void visitUserFunction(final UserDefinedFunction function) {
515
            inlineable = false;
×
516
        }
×
517

518
        @Override
519
        public void visitBuiltinFunction(final Function function) {
520
            inlineable = false;
1✔
521
        }
1✔
522

523
        @Override
524
        public void visitFunctionCall(final FunctionCall call) {
525
            inlineable = false;
1✔
526
        }
1✔
527

528
        @Override
529
        public void visitForExpression(final ForExpr forExpr) {
530
            inlineable = false;
×
531
        }
×
532

533
        @Override
534
        public void visitLetExpression(final LetExpr letExpr) {
535
            inlineable = false;
×
536
        }
×
537

538
        @Override
539
        public void visitOrderByClause(final OrderByClause orderBy) {
540
            inlineable = false;
×
541
        }
×
542

543
        @Override
544
        public void visitGroupByClause(final GroupByClause groupBy) {
545
            inlineable = false;
×
546
        }
×
547

548
        @Override
549
        public void visitWhereClause(final WhereClause where) {
550
            inlineable = false;
×
551
        }
×
552

553
        @Override
554
        public void visitConditional(final ConditionalExpression conditional) {
555
            inlineable = false;
×
556
        }
×
557

558
        @Override
559
        public void visitLocationStep(final LocationStep locationStep) {
560
        }
×
561

562
        @Override
563
        public void visitPredicate(final Predicate predicate) {
564
            super.visitPredicate(predicate);
×
565
        }
×
566

567
        @Override
568
        public void visitDocumentConstructor(final DocumentConstructor constructor) {
569
            inlineable = false;
1✔
570
        }
1✔
571

572
        @Override
573
        public void visitElementConstructor(final ElementConstructor constructor) {
574
            inlineable = false;
1✔
575
        }
1✔
576

577
        @Override
578
        public void visitTextConstructor(final DynamicTextConstructor constructor) {
579
            inlineable = false;
×
580
        }
×
581

582
        @Override
583
        public void visitAttribConstructor(final AttributeConstructor constructor) {
584
            inlineable = false;
×
585
        }
×
586

587
        @Override
588
        public void visitAttribConstructor(final DynamicAttributeConstructor constructor) {
589
            inlineable = false;
×
590
        }
×
591

592
        @Override
593
        public void visitUnionExpr(final Union union) {
594
            inlineable = false;
×
595
        }
×
596

597
        @Override
598
        public void visitIntersectionExpr(final Intersect intersect) {
599
            inlineable = false;
×
600
        }
×
601

602
        @Override
603
        public void visitVariableDeclaration(final VariableDeclaration decl) {
604
            inlineable = false;
×
605
        }
×
606

607
        @Override
608
        public void visitTryCatch(final TryCatchExpression tryCatch) {
609
            inlineable = false;
×
610
        }
×
611

612
        @Override
613
        public void visitCastExpr(final CastExpression expression) {
614
            inlineable = false;
1✔
615
        }
1✔
616

617
        @Override
618
        public void visitGeneralComparison(GeneralComparison comparison) {
619
            inlineable = false;
1✔
620
        }
1✔
621

622
        @Override
623
        public void visitAndExpr(final OpAnd and) {
624
            inlineable = false;
×
625
        }
×
626

627
        @Override
628
        public void visitOrExpr(final OpOr or) {
629
            inlineable = false;
×
630
        }
×
631

632
        @Override
633
        public void visitFilteredExpr(final FilteredExpression filtered) {
634
            inlineable = false;
×
635
        }
×
636

637
        @Override
638
        public void visitVariableReference(final VariableReference ref) {
639
            inlineable = false;
×
640
        }
×
641
    }
642
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc