parseJsonSync static method

Map<String, HitMap> parseJsonSync(
  1. List<Map<String, dynamic>> jsonResult, {
  2. required bool checkIgnoredLines,
  3. required Map<String, List<List<int>>?> ignoredLinesInFilesCache,
  4. required Resolver resolver,
})

Creates a single hitmap from a raw json object.

Note that when checkIgnoredLines is true all files will be read to get ignore comments. This will add some overhead. To combat this when calling this function multiple times from the same source (e.g. test runs of different files) a cache is taken via ignoredLinesInFilesCache. If this cache contains the parsed data for the specific file already, the file will not be read and parsed again.

Throws away all entries that are not resolvable.

Implementation

static Map<String, HitMap> parseJsonSync(
  List<Map<String, dynamic>> jsonResult, {
  required bool checkIgnoredLines,
  required Map<String, List<List<int>>?> ignoredLinesInFilesCache,
  required Resolver resolver,
}) {
  final loader = Loader();

  // Map of source file to map of line to hit count for that line.
  final globalHitMap = <String, HitMap>{};

  for (var e in jsonResult) {
    final source = e['source'] as String?;
    if (source == null) {
      // Couldn't resolve import, so skip this entry.
      continue;
    }

    var ignoredLinesList = <List<int>>[];

    if (checkIgnoredLines) {
      if (ignoredLinesInFilesCache.containsKey(source)) {
        final cacheHit = ignoredLinesInFilesCache[source];
        if (cacheHit == null) {
          // Null-entry indicates that the whole file was ignored.
          continue;
        }
        ignoredLinesList = cacheHit;
      } else {
        final path = resolver.resolve(source);
        if (path != null) {
          final lines = loader.loadSync(path) ?? [];
          ignoredLinesList = getIgnoredLines(path, lines);

          // Ignore the whole file.
          if (ignoredLinesList.length == 1 &&
              ignoredLinesList[0][0] == 0 &&
              ignoredLinesList[0][1] == lines.length) {
            // Null-entry indicates that the whole file was ignored.
            ignoredLinesInFilesCache[source] = null;
            continue;
          }
          ignoredLinesInFilesCache[source] = ignoredLinesList;
        } else {
          // Couldn't resolve source. Allow cache to answer next time
          // anyway.
          ignoredLinesInFilesCache[source] = ignoredLinesList;
        }
      }
    }

    // Move to the first ignore range.
    final ignoredLines = ignoredLinesList.iterator;
    var hasCurrent = ignoredLines.moveNext();

    bool shouldIgnoreLine(Iterator<List<int>> ignoredRanges, int line) {
      if (!hasCurrent || ignoredRanges.current.isEmpty) {
        return false;
      }

      if (line < ignoredRanges.current[0]) return false;

      while (hasCurrent &&
          ignoredRanges.current.isNotEmpty &&
          ignoredRanges.current[1] < line) {
        hasCurrent = ignoredRanges.moveNext();
      }

      if (hasCurrent &&
          ignoredRanges.current.isNotEmpty &&
          ignoredRanges.current[0] <= line &&
          line <= ignoredRanges.current[1]) {
        return true;
      }

      return false;
    }

    void addToMap(Map<int, int> map, int line, int count) {
      final oldCount = map.putIfAbsent(line, () => 0);
      map[line] = count + oldCount;
    }

    void fillHitMap(List hits, Map<int, int> hitMap) {
      // Ignore line annotations require hits to be sorted.
      hits = _sortHits(hits);
      // hits is a flat array of the following format:
      // [ <line|linerange>, <hitcount>,...]
      // line: number.
      // linerange: '<line>-<line>'.
      for (var i = 0; i < hits.length; i += 2) {
        final k = hits[i];
        if (k is int) {
          // Single line.
          if (shouldIgnoreLine(ignoredLines, k)) continue;

          addToMap(hitMap, k, hits[i + 1] as int);
        } else if (k is String) {
          // Linerange. We expand line ranges to actual lines at this point.
          final splitPos = k.indexOf('-');
          final start = int.parse(k.substring(0, splitPos));
          final end = int.parse(k.substring(splitPos + 1));
          for (var j = start; j <= end; j++) {
            if (shouldIgnoreLine(ignoredLines, j)) continue;

            addToMap(hitMap, j, hits[i + 1] as int);
          }
        } else {
          throw StateError('Expected value of type int or String');
        }
      }
    }

    final sourceHitMap = globalHitMap.putIfAbsent(source, HitMap.new);
    fillHitMap(e['hits'] as List, sourceHitMap.lineHits);
    if (e.containsKey('funcHits')) {
      sourceHitMap.funcHits ??= <int, int>{};
      fillHitMap(e['funcHits'] as List, sourceHitMap.funcHits!);
    }
    if (e.containsKey('funcNames')) {
      sourceHitMap.funcNames ??= <int, String>{};
      final funcNames = e['funcNames'] as List;
      for (var i = 0; i < funcNames.length; i += 2) {
        sourceHitMap.funcNames![funcNames[i] as int] =
            funcNames[i + 1] as String;
      }
    }
    if (e.containsKey('branchHits')) {
      sourceHitMap.branchHits ??= <int, int>{};
      fillHitMap(e['branchHits'] as List, sourceHitMap.branchHits!);
    }
  }
  return globalHitMap;
}