annotator.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. /*
  2. Copyright 2012-2015, Yahoo Inc.
  3. Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
  4. */
  5. "use strict";
  6. var InsertionText = require('./insertion-text'),
  7. lt = '\u0001',
  8. gt = '\u0002',
  9. RE_LT = /</g,
  10. RE_GT = />/g,
  11. RE_AMP = /&/g,
  12. RE_lt = /\u0001/g,
  13. RE_gt = /\u0002/g;
  14. function title(str) {
  15. return ' title="' + str + '" ';
  16. }
  17. function customEscape(text) {
  18. text = String(text);
  19. return text.replace(RE_AMP, '&amp;')
  20. .replace(RE_LT, '&lt;')
  21. .replace(RE_GT, '&gt;')
  22. .replace(RE_lt, '<')
  23. .replace(RE_gt, '>');
  24. }
  25. function annotateLines(fileCoverage, structuredText) {
  26. var lineStats = fileCoverage.getLineCoverage();
  27. if (!lineStats) {
  28. return;
  29. }
  30. Object.keys(lineStats).forEach(function (lineNumber) {
  31. var count = lineStats[lineNumber];
  32. if (structuredText[lineNumber]) {
  33. structuredText[lineNumber].covered = count > 0 ? 'yes' : 'no';
  34. structuredText[lineNumber].hits = count;
  35. }
  36. });
  37. }
  38. function annotateStatements(fileCoverage, structuredText) {
  39. var statementStats = fileCoverage.s,
  40. statementMeta = fileCoverage.statementMap;
  41. Object.keys(statementStats).forEach(function (stName) {
  42. var count = statementStats[stName],
  43. meta = statementMeta[stName],
  44. type = count > 0 ? 'yes' : 'no',
  45. startCol = meta.start.column,
  46. endCol = meta.end.column + 1,
  47. startLine = meta.start.line,
  48. endLine = meta.end.line,
  49. openSpan = lt + 'span class="' + (meta.skip ? 'cstat-skip' : 'cstat-no') + '"' + title('statement not covered') + gt,
  50. closeSpan = lt + '/span' + gt,
  51. text;
  52. if (type === 'no' && structuredText[startLine]) {
  53. if (endLine !== startLine) {
  54. endCol = structuredText[startLine].text.originalLength();
  55. }
  56. text = structuredText[startLine].text;
  57. text.wrap(startCol,
  58. openSpan,
  59. startCol < endCol ? endCol : text.originalLength(),
  60. closeSpan);
  61. }
  62. });
  63. }
  64. function annotateFunctions(fileCoverage, structuredText) {
  65. var fnStats = fileCoverage.f,
  66. fnMeta = fileCoverage.fnMap;
  67. if (!fnStats) {
  68. return;
  69. }
  70. Object.keys(fnStats).forEach(function (fName) {
  71. var count = fnStats[fName],
  72. meta = fnMeta[fName],
  73. type = count > 0 ? 'yes' : 'no',
  74. startCol = meta.decl.start.column,
  75. endCol = meta.decl.end.column + 1,
  76. startLine = meta.decl.start.line,
  77. endLine = meta.decl.end.line,
  78. openSpan = lt + 'span class="' + (meta.skip ? 'fstat-skip' : 'fstat-no') + '"' + title('function not covered') + gt,
  79. closeSpan = lt + '/span' + gt,
  80. text;
  81. if (type === 'no' && structuredText[startLine]) {
  82. if (endLine !== startLine) {
  83. endCol = structuredText[startLine].text.originalLength();
  84. }
  85. text = structuredText[startLine].text;
  86. text.wrap(startCol,
  87. openSpan,
  88. startCol < endCol ? endCol : text.originalLength(),
  89. closeSpan);
  90. }
  91. });
  92. }
  93. function annotateBranches(fileCoverage, structuredText) {
  94. var branchStats = fileCoverage.b,
  95. branchMeta = fileCoverage.branchMap;
  96. if (!branchStats) {
  97. return;
  98. }
  99. Object.keys(branchStats).forEach(function (branchName) {
  100. var branchArray = branchStats[branchName],
  101. sumCount = branchArray.reduce(function (p, n) {
  102. return p + n;
  103. }, 0),
  104. metaArray = branchMeta[branchName].locations,
  105. i,
  106. count,
  107. meta,
  108. type,
  109. startCol,
  110. endCol,
  111. startLine,
  112. endLine,
  113. openSpan,
  114. closeSpan,
  115. text;
  116. // only highlight if partial branches are missing or if there is a
  117. // single uncovered branch.
  118. if (sumCount > 0 || (sumCount === 0 && branchArray.length === 1)) {
  119. for (i = 0; i < branchArray.length && i < metaArray.length; i += 1) {
  120. count = branchArray[i];
  121. meta = metaArray[i];
  122. type = count > 0 ? 'yes' : 'no';
  123. startCol = meta.start.column;
  124. endCol = meta.end.column + 1;
  125. startLine = meta.start.line;
  126. endLine = meta.end.line;
  127. openSpan = lt + 'span class="branch-' + i + ' ' +
  128. (meta.skip ? 'cbranch-skip' : 'cbranch-no') + '"'
  129. + title('branch not covered') + gt;
  130. closeSpan = lt + '/span' + gt;
  131. if (count === 0 && structuredText[startLine]) { //skip branches taken
  132. if (endLine !== startLine) {
  133. endCol = structuredText[startLine].text.originalLength();
  134. }
  135. text = structuredText[startLine].text;
  136. if (branchMeta[branchName].type === 'if') {
  137. // 'if' is a special case
  138. // since the else branch might not be visible, being non-existent
  139. text.insertAt(startCol, lt + 'span class="' +
  140. (meta.skip ? 'skip-if-branch' : 'missing-if-branch') + '"' +
  141. title((i === 0 ? 'if' : 'else') + ' path not taken') + gt +
  142. (i === 0 ? 'I' : 'E') + lt + '/span' + gt, true, false);
  143. } else {
  144. text.wrap(startCol,
  145. openSpan,
  146. startCol < endCol ? endCol : text.originalLength(),
  147. closeSpan);
  148. }
  149. }
  150. }
  151. }
  152. });
  153. }
  154. function annotateSourceCode(fileCoverage, sourceStore) {
  155. var codeArray,
  156. lineCoverageArray;
  157. try {
  158. var sourceText = sourceStore.getSource(fileCoverage.path),
  159. code = sourceText.split(/(?:\r?\n)|\r/),
  160. count = 0,
  161. structured = code.map(function (str) {
  162. count += 1;
  163. return {
  164. line: count,
  165. covered: 'neutral',
  166. hits: 0,
  167. text: new InsertionText(str, true)
  168. };
  169. });
  170. structured.unshift({line: 0, covered: null, text: new InsertionText("")});
  171. annotateLines(fileCoverage, structured);
  172. //note: order is important, since statements typically result in spanning the whole line and doing branches late
  173. //causes mismatched tags
  174. annotateBranches(fileCoverage, structured);
  175. annotateFunctions(fileCoverage, structured);
  176. annotateStatements(fileCoverage, structured);
  177. structured.shift();
  178. codeArray = structured.map(function (item) {
  179. return customEscape(item.text.toString()) || '&nbsp;';
  180. });
  181. lineCoverageArray = structured.map(function (item) {
  182. return {
  183. covered: item.covered,
  184. hits: item.hits > 0 ? item.hits + 'x' : '&nbsp;'
  185. };
  186. });
  187. return {
  188. annotatedCode: codeArray,
  189. lineCoverage: lineCoverageArray,
  190. maxLines: structured.length
  191. };
  192. } catch (ex) {
  193. codeArray = [ ex.message ];
  194. lineCoverageArray = [ { covered: 'no', hits: 0 } ];
  195. String(ex.stack || '').split(/\r?\n/).forEach(function (line) {
  196. codeArray.push(line);
  197. lineCoverageArray.push({ covered: 'no', hits: 0 });
  198. });
  199. return {
  200. annotatedCode: codeArray,
  201. lineCoverage: lineCoverageArray,
  202. maxLines: codeArray.length
  203. };
  204. }
  205. }
  206. module.exports = {
  207. annotateSourceCode: annotateSourceCode
  208. };