This is an automated email from the ASF dual-hosted git repository. wusheng pushed a commit to branch mal-filter-fix in repository https://gitbox.apache.org/repos/asf/skywalking.git
commit 7ddf09dc3c8bd5b7c98ed9625e7b42d55846ea39 Author: Wu Sheng <[email protected]> AuthorDate: Sat Mar 7 00:38:21 2026 +0800 Fix v2 MAL compiler: sanitizeName, filter class generation, Groovy truth semantics 1. sanitizeName: prepend '_' for numeric-prefixed metric names (e.g., 4xx → _4xx) instead of replacing the first character, which lost the digit. 2. Generate v2 filter .class files into generated-classes/ directories during v1-v2 checker tests (56 filter classes). Includes deterministic naming via configPath-based hints in production FilterExpression. 3. Set SourceFile attributes with YAML filename and line number on both expression and filter generated classes for stack trace traceability. 4. Implement Groovy truth semantics via MalRuntimeHelper.isTruthy() for filter closure codegen — null → false, empty string → false, Boolean.FALSE → false, matching Groovy behavior for expressions like `tags.ApiId || tags.ApiName`. --- .gitignore | 2 + .../log/analyzer/v2/compiler/LALCodegenHelper.java | 14 ++-- .../skywalking/oap/meter/analyzer/v2/Analyzer.java | 10 ++- .../oap/meter/analyzer/v2/MetricConvert.java | 24 +++++- .../oap/meter/analyzer/v2/MetricRuleConfig.java | 11 +++ .../analyzer/v2/compiler/MALClassGenerator.java | 2 + .../analyzer/v2/compiler/MALClosureCodegen.java | 5 +- .../analyzer/v2/compiler/MALCodegenHelper.java | 14 ++-- .../analyzer/v2/compiler/rt/MalRuntimeHelper.java | 19 +++++ .../meter/analyzer/v2/dsl/FilterExpression.java | 13 +++- .../meter/analyzer/v2/prometheus/rule/Rule.java | 1 + .../meter/analyzer/v2/prometheus/rule/Rules.java | 6 +- .../oap/server/checker/mal/MalComparisonTest.java | 89 +++++++++++++++++++++- 13 files changed, 190 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index f1e59ed33f..0efd7f68c2 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ OALLexer.tokens .checkstyle .externalToolBuilders oap-server/oal-grammar/**/gen/ +oap-server/analyzer/log-analyzer/gen/ +oap-server/analyzer/meter-analyzer/gen/ MQELexer.tokens oap-server/mqe-grammar/**/gen/ PromQLLexer.tokens diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java index 0947ee263c..bb890a65fe 100644 --- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java +++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALCodegenHelper.java @@ -80,14 +80,18 @@ final class LALCodegenHelper { } static String sanitizeName(final String name) { - final StringBuilder sb = new StringBuilder(name.length()); + if (name == null || name.isEmpty()) { + return "Generated"; + } + final StringBuilder sb = new StringBuilder(name.length() + 1); + if (!Character.isJavaIdentifierStart(name.charAt(0))) { + sb.append('_'); + } for (int i = 0; i < name.length(); i++) { final char c = name.charAt(i); - sb.append(i == 0 - ? (Character.isJavaIdentifierStart(c) ? c : '_') - : (Character.isJavaIdentifierPart(c) ? c : '_')); + sb.append(Character.isJavaIdentifierPart(c) ? c : '_'); } - return sb.length() == 0 ? "Generated" : sb.toString(); + return sb.toString(); } static String generateMapValCall(final List<String> keys) { diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/Analyzer.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/Analyzer.java index d07fcfde40..fa65613cbe 100644 --- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/Analyzer.java +++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/Analyzer.java @@ -115,11 +115,19 @@ public class Analyzer { final String expression, final MeterSystem meterSystem, final String yamlSource) { - Expression e = DSL.parse(metricName, expression, yamlSource); FilterExpression filter = null; if (!Strings.isNullOrEmpty(filterExpression)) { filter = new FilterExpression(filterExpression); } + return build(metricName, filter, expression, meterSystem, yamlSource); + } + + public static Analyzer build(final String metricName, + final FilterExpression filter, + final String expression, + final MeterSystem meterSystem, + final String yamlSource) { + Expression e = DSL.parse(metricName, expression, yamlSource); ExpressionMetadata ctx = e.parse(); Analyzer analyzer = new Analyzer(metricName, filter, e, meterSystem, ctx); analyzer.init(); diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/MetricConvert.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/MetricConvert.java index 2891937864..3dafbb4e87 100644 --- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/MetricConvert.java +++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/MetricConvert.java @@ -28,6 +28,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.skywalking.oap.meter.analyzer.v2.dsl.FilterExpression; import org.apache.skywalking.oap.meter.analyzer.v2.dsl.SampleFamily; import org.apache.skywalking.oap.server.core.analysis.meter.MeterSystem; @@ -75,6 +76,7 @@ public class MetricConvert { public MetricConvert(MetricRuleConfig rule, MeterSystem service) { Preconditions.checkState(!Strings.isNullOrEmpty(rule.getMetricPrefix())); final String sourceName = rule.getSourceName(); + final FilterExpression filter = buildFilter(rule); final List<? extends MetricRuleConfig.RuleConfig> rules = rule.getMetricsRules(); this.analyzers = IntStream.range(0, rules.size()).mapToObj( i -> { @@ -83,7 +85,7 @@ public class MetricConvert { ? sourceName + ".yaml:" + i : null; return buildAnalyzer( formatMetricName(rule, r.getName()), - rule.getFilter(), + filter, formatExp(rule.getExpPrefix(), rule.getExpSuffix(), r.getExp()), service, yamlSource @@ -93,7 +95,7 @@ public class MetricConvert { } Analyzer buildAnalyzer(final String metricsName, - final String filter, + final FilterExpression filter, final String exp, final MeterSystem service, final String yamlSource) { @@ -106,6 +108,24 @@ public class MetricConvert { ); } + private static FilterExpression buildFilter(final MetricRuleConfig rule) { + final String filterText = rule.getFilter(); + if (Strings.isNullOrEmpty(filterText)) { + return null; + } + final String configPath = rule.getConfigPath(); + final String sourceName = rule.getSourceName(); + final StringBuilder hint = new StringBuilder(); + if (!Strings.isNullOrEmpty(configPath)) { + hint.append(configPath).append('_'); + } + if (!Strings.isNullOrEmpty(sourceName)) { + hint.append(sourceName).append('_'); + } + hint.append("filter"); + return new FilterExpression(filterText, hint.toString()); + } + private String formatExp(final String expPrefix, String expSuffix, String exp) { String ret = exp; if (!Strings.isNullOrEmpty(expPrefix)) { diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/MetricRuleConfig.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/MetricRuleConfig.java index 42f55eebeb..a7f91db0f9 100644 --- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/MetricRuleConfig.java +++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/MetricRuleConfig.java @@ -58,6 +58,17 @@ public interface MetricRuleConfig { return null; } + /** + * Returns the config path (directory) from which this config was loaded + * (e.g., {@code "otel-rules"}, {@code "envoy-metrics-rules"}). + * Used to build deterministic filter class names. + * + * @return config path, or {@code null} if unknown. + */ + default String getConfigPath() { + return null; + } + interface RuleConfig { /** * Get definition metrics name diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java index e55a50bfe5..833ff4546a 100644 --- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java +++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java @@ -372,6 +372,8 @@ public final class MALClassGenerator { {paramName, "Ljava/util/Map;"} }); addLineNumberTable(testMethod, 2); // slot 0=this, 1=samples + setSourceFile(ctClass, formatSourceFileName( + classNameHint != null ? classNameHint : "filter")); writeClassFile(ctClass); diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClosureCodegen.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClosureCodegen.java index 4db10cf2b9..254289b4f5 100644 --- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClosureCodegen.java +++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClosureCodegen.java @@ -761,9 +761,10 @@ final class MALClosureCodegen { if (MALCodegenHelper.isBooleanExpression(condExpr)) { generateClosureExpr(sb, condExpr, paramName, beanMode); } else { - sb.append("("); + // Groovy truth: non-null, non-empty string, non-false + sb.append(MALCodegenHelper.RUNTIME_HELPER_FQCN).append(".isTruthy("); generateClosureExpr(sb, condExpr, paramName, beanMode); - sb.append(" != null)"); + sb.append(")"); } } else if (cond instanceof MALExpressionModel.ClosureInCondition) { final MALExpressionModel.ClosureInCondition ic = diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALCodegenHelper.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALCodegenHelper.java index 072a7e2b4a..8f02be1df5 100644 --- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALCodegenHelper.java +++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALCodegenHelper.java @@ -102,14 +102,18 @@ final class MALCodegenHelper { // ---- Static utility methods ---- static String sanitizeName(final String name) { - final StringBuilder sb = new StringBuilder(name.length()); + if (name == null || name.isEmpty()) { + return "Generated"; + } + final StringBuilder sb = new StringBuilder(name.length() + 1); + if (!Character.isJavaIdentifierStart(name.charAt(0))) { + sb.append('_'); + } for (int i = 0; i < name.length(); i++) { final char c = name.charAt(i); - sb.append(i == 0 - ? (Character.isJavaIdentifierStart(c) ? c : '_') - : (Character.isJavaIdentifierPart(c) ? c : '_')); + sb.append(Character.isJavaIdentifierPart(c) ? c : '_'); } - return sb.length() == 0 ? "Generated" : sb.toString(); + return sb.toString(); } static String escapeJava(final String s) { diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/rt/MalRuntimeHelper.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/rt/MalRuntimeHelper.java index 1e8dfc0d5f..669be6ef7c 100644 --- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/rt/MalRuntimeHelper.java +++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/rt/MalRuntimeHelper.java @@ -59,6 +59,25 @@ public final class MalRuntimeHelper { * Reverse division: computes {@code numerator / v} for each sample value {@code v}. * Used by generated code for {@code Number / SampleFamily} expressions. */ + /** + * Groovy truth check: {@code null → false}, empty string → {@code false}, + * {@code Boolean.FALSE → false}, everything else → {@code true}. + * Used by generated filter code for standalone expressions in boolean context + * (e.g., {@code tags.ApiId || tags.ApiName}). + */ + public static boolean isTruthy(final Object value) { + if (value == null) { + return false; + } + if (value instanceof Boolean) { + return (Boolean) value; + } + if (value instanceof CharSequence) { + return ((CharSequence) value).length() > 0; + } + return true; + } + public static SampleFamily divReverse(final double numerator, final SampleFamily sf) { if (sf == SampleFamily.EMPTY) { diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/FilterExpression.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/FilterExpression.java index f5fd74696a..c69d87dffd 100644 --- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/FilterExpression.java +++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/dsl/FilterExpression.java @@ -37,9 +37,20 @@ public class FilterExpression { private final MalFilter malFilter; public FilterExpression(final String literal) { + this(literal, null); + } + + public FilterExpression(final String literal, final String filterNameHint) { this.literal = literal; try { - this.malFilter = GENERATOR.compileFilter(literal); + if (filterNameHint != null) { + GENERATOR.setClassNameHint(filterNameHint); + } + try { + this.malFilter = GENERATOR.compileFilter(literal); + } finally { + GENERATOR.setClassNameHint(null); + } } catch (Exception e) { throw new IllegalStateException( "Failed to compile MAL filter expression: " + literal, e); diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/prometheus/rule/Rule.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/prometheus/rule/Rule.java index 930b2e7718..e6e1f39d72 100644 --- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/prometheus/rule/Rule.java +++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/prometheus/rule/Rule.java @@ -31,6 +31,7 @@ import java.util.List; @NoArgsConstructor public class Rule implements MetricRuleConfig { private String name; + private String configPath; private String metricPrefix; private String expSuffix; private String expPrefix; diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/prometheus/rule/Rules.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/prometheus/rule/Rules.java index c179e774a7..a70618d9bd 100644 --- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/prometheus/rule/Rules.java +++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/prometheus/rule/Rules.java @@ -86,7 +86,11 @@ public class Rules { // Use relativized file path without suffix as the rule name. String relativizePath = root.relativize(pathPointer).toString(); String ruleName = relativizePath.substring(0, relativizePath.lastIndexOf(".")); - return getRulesFromFile(ruleName, pathPointer); + Rule rule = getRulesFromFile(ruleName, pathPointer); + if (rule != null) { + rule.setConfigPath(path); + } + return rule; }) .filter(Objects::nonNull) .collect(Collectors.toList()) ; diff --git a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java index 1f84691350..c8de43dfa5 100644 --- a/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java +++ b/test/script-cases/script-runtime-with-groovy/mal-lal-v1-v2-checker/src/test/java/org/apache/skywalking/oap/server/checker/mal/MalComparisonTest.java @@ -130,9 +130,29 @@ class MalComparisonTest { final List<DynamicTest> tests = new ArrayList<>(); final Map<String, List<MalRule>> yamlRules = loadAllMalYamlFiles(); + // Compile v2 filter once per source file — generates .class into generated-classes/ + final java.util.Set<File> compiledFilters = new java.util.HashSet<>(); + for (final Map.Entry<String, List<MalRule>> entry : yamlRules.entrySet()) { final String yamlFile = entry.getKey(); for (final MalRule rule : entry.getValue()) { + // Compile v2 filter once per source file (generates .class file) + if (rule.filter != null && rule.sourceFile != null + && compiledFilters.add(rule.sourceFile)) { + final String baseName = rule.sourceFile.getName() + .replaceFirst("\\.(yaml|yml)$", ""); + final int filterLine = findFilterLine(rule.sourceFile); + final MALClassGenerator filterGen = new MALClassGenerator(); + filterGen.setClassOutputDir(new java.io.File( + rule.sourceFile.getParent(), + baseName + ".generated-classes")); + filterGen.setClassNameHint(baseName + "_filter"); + filterGen.setYamlSource(filterLine > 0 + ? rule.sourceFile.getName() + ":" + filterLine + : rule.sourceFile.getName()); + filterGen.compileFilter(rule.filter); + } + // Compile v2 once per metric — compilation is independent of input data org.apache.skywalking.oap.meter.analyzer.v2.dsl.MalExpression v2Expr = null; ExpressionMetadata v2Meta = null; @@ -167,6 +187,9 @@ class MalComparisonTest { rule.sourceFile.getParent(), baseName + ".generated-classes")); generator.setClassNameHint(rule.name); + generator.setYamlSource(rule.lineNo > 0 + ? rule.sourceFile.getName() + ":" + rule.lineNo + : rule.sourceFile.getName()); } return generator.compile(rule.name, rule.fullExpression); } @@ -223,7 +246,8 @@ class MalComparisonTest { (Map<String, Object>) rule.inputConfig.get("expected"); if (inputSection != null) { compareExecutionWithInput( - rule, v1Expr, v2MalExpr, v2Meta, inputSection, expectedSection); + rule, v1Expr, v2MalExpr, v2Meta, + inputSection, expectedSection); return; } } @@ -1007,6 +1031,9 @@ class MalComparisonTest { final Object rawMetricPrefix = config.get("metricPrefix"); final String metricPrefix = rawMetricPrefix instanceof String ? (String) rawMetricPrefix : null; + final Object rawFilter = config.get("filter"); + final String filter = rawFilter instanceof String + ? ((String) rawFilter).trim() : null; // Support both "metricsRules" (standard) and "metrics" (zabbix) List<Map<String, String>> rules = (List<Map<String, String>>) config.get("metricsRules"); @@ -1029,6 +1056,8 @@ class MalComparisonTest { final String yamlName = prefix + "/" + file.getName(); final List<MalRule> malRules = new ArrayList<>(); final Map<String, Integer> nameCount = new HashMap<>(); + // Build line number index: find "name: <value>" lines + final String[] lines = content.split("\n"); for (final Map<String, String> rule : rules) { final String name = rule.get("name"); final String exp = rule.get("exp"); @@ -1039,7 +1068,14 @@ class MalComparisonTest { final int count = nameCount.merge(name, 1, Integer::sum); final String uniqueName = count > 1 ? name + "_" + count : name; final String fullExp = formatExp(expPrefix, expSuffix, exp); - malRules.add(new MalRule(uniqueName, fullExp, inputConfig, metricPrefix, file)); + // Extract top-level dir name (e.g., "test-otel-rules") from prefix + final String dirName = prefix.contains("/") + ? prefix.substring(0, prefix.indexOf('/')) : prefix; + // Find line number of this rule's "name:" in YAML + final int lineNo = findRuleLine(lines, name, count); + malRules.add(new MalRule( + uniqueName, fullExp, inputConfig, metricPrefix, + file, filter, dirName, lineNo)); } if (!malRules.isEmpty()) { result.put(yamlName, malRules); @@ -1061,6 +1097,46 @@ class MalComparisonTest { return null; } + /** + * Find the 1-based line number of the {@code filter:} field in a YAML file. + */ + private static int findFilterLine(final File yamlFile) { + try { + final String[] lines = Files.readString(yamlFile.toPath()).split("\n"); + for (int i = 0; i < lines.length; i++) { + if (lines[i].trim().startsWith("filter:")) { + return i + 1; + } + } + } catch (Exception ignored) { + } + return 0; + } + + /** + * Find the 1-based line number of the Nth occurrence of {@code name: <value>} in the YAML. + */ + private static int findRuleLine(final String[] lines, final String name, + final int occurrence) { + int found = 0; + for (int i = 0; i < lines.length; i++) { + String trimmed = lines[i].trim(); + // Strip YAML list prefix "- " + if (trimmed.startsWith("- ")) { + trimmed = trimmed.substring(2); + } + if (trimmed.equals("name: " + name) + || trimmed.equals("name: '" + name + "'") + || trimmed.equals("name: \"" + name + "\"")) { + found++; + if (found == occurrence) { + return i + 1; + } + } + } + return 0; + } + /** * Replicates the production {@code MetricConvert.formatExp()} logic: * inserts {@code expPrefix} after the metric name (first dot-segment), @@ -1093,15 +1169,22 @@ class MalComparisonTest { final Map<String, Object> inputConfig; final String metricPrefix; final File sourceFile; + final String filter; + final String dirName; + final int lineNo; MalRule(final String name, final String fullExpression, final Map<String, Object> inputConfig, final String metricPrefix, - final File sourceFile) { + final File sourceFile, final String filter, final String dirName, + final int lineNo) { this.name = name; this.fullExpression = fullExpression; this.inputConfig = inputConfig; this.metricPrefix = metricPrefix; this.sourceFile = sourceFile; + this.filter = filter; + this.dirName = dirName; + this.lineNo = lineNo; } } }
