]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6765 SONAR-6766 show multiple issue locations and execution flows
authorStas Vilchik <vilchiks@gmail.com>
Wed, 12 Aug 2015 09:23:22 +0000 (11:23 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Thu, 13 Aug 2015 08:49:41 +0000 (10:49 +0200)
14 files changed:
server/sonar-web/src/main/js/apps/issues/component-viewer/main.js
server/sonar-web/src/main/js/components/issue/issue-view.js
server/sonar-web/src/main/js/components/issue/templates/issue.hbs
server/sonar-web/src/main/js/components/source-viewer/helpers/code-with-issue-locations-helper.js
server/sonar-web/src/main/js/components/source-viewer/main.js
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-flow-location.hbs [new file with mode: 0644]
server/sonar-web/src/main/less/components/source.less
server/sonar-web/src/main/less/init/icons.less
server/sonar-web/src/main/less/pages/issues.less
server/sonar-web/src/main/less/variables.less
server/sonar-web/src/test/json/source-viewer-spec/issues-with-precise-location.json
server/sonar-web/test/intern.js
server/sonar-web/test/medium/source-viewer.spec.js
server/sonar-web/test/unit/code-with-issue-locations-helper.spec.js [new file with mode: 0644]

index 1e79d92e45dd580fdccc9e74da635275f9512a39..8792113e2136783b7064cc43ab6d496f7a6e2cb2 100644 (file)
@@ -24,6 +24,7 @@ define([
       SourceViewer.prototype.onLoaded.apply(this, arguments);
       this.bindShortcuts();
       if (this.baseIssue != null) {
+        this.baseIssue.trigger('locations', this.baseIssue);
         return this.scrollToLine(this.baseIssue.get('line'));
       }
     },
@@ -83,6 +84,7 @@ define([
       var selected = this.options.app.state.get('selectedIndex'),
           selectedIssue = this.options.app.list.at(selected);
       if (selectedIssue.get('component') === this.model.get('key')) {
+        selectedIssue.trigger('locations', selectedIssue);
         return this.scrollToIssue(selectedIssue.get('key'));
       } else {
         this.unbindShortcuts();
index 14e83e689e4f6392087152c181d4fc3c5e290eff..aad34a2d04c39e8d4e2d8ce8045233d667390456 100644 (file)
@@ -37,7 +37,8 @@ define([
         'click .js-issue-plan': 'plan',
         'click .js-issue-show-changelog': 'showChangeLog',
         'click .js-issue-rule': 'showRule',
-        'click .js-issue-edit-tags': 'editTags'
+        'click .js-issue-edit-tags': 'editTags',
+        'click .js-issue-locations': 'showLocations'
       };
     },
 
@@ -217,6 +218,10 @@ define([
       this.popup.render();
     },
 
+    showLocations: function () {
+      this.model.trigger('locations', this.model);
+    },
+
     serializeData: function () {
       var issueKey = encodeURIComponent(this.model.get('key'));
       return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
index 930e5ca569378bbcde5ca7d84dad4a00e442866d..06e878f7b8fe05411cefdd8a79d14965f3b7b385 100644 (file)
@@ -4,7 +4,8 @@
     <tr>
       <td>
         <div class="issue-message">
-          {{message}}&nbsp;<button class="button-link js-issue-rule issue-rule icon-ellipsis-h"></button>
+          {{message}}&nbsp;
+          <button class="button-link js-issue-rule issue-rule icon-ellipsis-h"></button>
         </div>
       </td>
 
             </li>
           {{/if}}
 
+          {{#notEmpty secondaryLocations}}
+            <li class="issue-meta issue-meta-locations">
+              <button class="button-link issue-action js-issue-locations">
+                <i class="icon-issue-flow"></i>
+              </button>
+            </li>
+          {{/notEmpty}}
+
           <li class="issue-meta">
             <a class="js-issue-permalink icon-link" href="{{permalink}}" target="_blank"></a>
           </li>
             {{#if updatable}}
               <button class="js-issue-comment-edit button-link icon-edit icon-half-transparent"></button>
               <button class="js-issue-comment-delete button-link icon-delete icon-half-transparent"
-                 data-confirm-msg="{{t 'issue.comment.delete_confirm_message'}}"></button>
+                      data-confirm-msg="{{t 'issue.comment.delete_confirm_message'}}"></button>
             {{/if}}
           </div>
         </div>
index b95c6efd18cdd1c23137ea2f62e72d149ecd6b98..fd5d56a3bde0940cd3a5058da7a90c52e20bb581 100644 (file)
@@ -22,7 +22,8 @@ define(function () {
    * @returns {string}
    */
   function part (str, from, to, acc) {
-    return str.substr(from - acc, to - from);
+    // we do not want negative number as the first argument of `substr`
+    return from >= acc ? str.substr(from - acc, to - from) : str.substr(0, to - from);
   }
 
 
@@ -53,9 +54,10 @@ define(function () {
    * Highlight issue locations in the list of tokens
    * @param {Array} tokens
    * @param {Array} issueLocations
+   * @param {string} className
    * @returns {Array}
    */
-  function highlightIssueLocations (tokens, issueLocations) {
+  function highlightIssueLocations (tokens, issueLocations, className) {
     issueLocations.forEach(function (location) {
       var nextTokens = [],
           acc = 0;
@@ -68,8 +70,8 @@ define(function () {
           nextTokens.push({ className: token.className, text: p1 });
         }
         if (p2.length) {
-          var newClassName = token.className.indexOf('source-line-code-issue') === -1 ?
-              [token.className, 'source-line-code-issue'].join(' ') : token.className;
+          var newClassName = token.className.indexOf(className) === -1 ?
+              [token.className, className].join(' ') : token.className;
           nextTokens.push({ className: newClassName, text: p2 });
         }
         if (p3.length) {
@@ -100,20 +102,26 @@ define(function () {
    * highlight issues and generate result html
    * @param {string} code
    * @param {Array} issueLocations
+   * @param {string} [optionalClassName]
    * @returns {string}
    */
-  function doTheStuff (code, issueLocations) {
+  function doTheStuff (code, issueLocations, optionalClassName) {
     var _code = code || '&nbsp;';
     var _issueLocations = issueLocations || [];
-    return generateHTML(highlightIssueLocations(splitByTokens(_code), _issueLocations));
+    var _className = optionalClassName ? optionalClassName : 'source-line-code-issue';
+    return generateHTML(highlightIssueLocations(splitByTokens(_code), _issueLocations, _className));
   }
 
 
-  /**
-   * Handlebars helper to highlight issue locations in the source code
-   */
-  Handlebars.registerHelper('codeWithIssueLocations', function (code, issueLocations) {
-    return doTheStuff(code, issueLocations);
-  });
+  if (typeof Handlebars !== 'undefined') {
+    /**
+     * Handlebars helper to highlight issue locations in the source code
+     */
+    Handlebars.registerHelper('codeWithIssueLocations', function (code, issueLocations) {
+      return doTheStuff(code, issueLocations);
+    });
+  }
+
+  return doTheStuff;
 
 });
index 828e9e53ffcb6011cb1f1eb9df2c4b9d60cb5625..a73b56a315f08e63b8e4ff1a3b2d923b9c520250 100644 (file)
@@ -39,7 +39,8 @@ define([
               SCMPopupView,
               CoveragePopupView,
               DuplicationPopupView,
-              LineActionsPopupView) {
+              LineActionsPopupView,
+              highlightLocations) {
 
       var $ = jQuery,
           HIGHLIGHTED_ROW_CLASS = 'source-line-highlighted';
@@ -47,6 +48,7 @@ define([
       return Marionette.LayoutView.extend({
         className: 'source-viewer',
         template: Templates['source-viewer'],
+        flowLocationTemplate: Templates['source-viewer-flow-location'],
 
         ISSUES_LIMIT: 3000,
         LINES_LIMIT: 1000,
@@ -84,6 +86,7 @@ define([
           }
           this.issues = new Issues();
           this.listenTo(this.issues, 'change:severity', this.onIssuesSeverityChange);
+          this.listenTo(this.issues, 'locations', this.toggleFlowLocations);
           this.issueViews = [];
           this.loadSourceBeforeThrottled = _.throttle(this.loadSourceBefore, 1000);
           this.loadSourceAfterThrottled = _.throttle(this.loadSourceAfter, 1000);
@@ -277,8 +280,8 @@ define([
                 data: {
                   componentUuids: this.model.id,
                   f: 'component,componentId,project,subProject,rule,status,resolution,author,reporter,assignee,debt,' +
-                    'line,message,severity,actionPlan,creationDate,updateDate,closeDate,tags,comments,attr,actions,' +
-                    'transitions,actionPlanName',
+                  'line,message,severity,actionPlan,creationDate,updateDate,closeDate,tags,comments,attr,actions,' +
+                  'transitions,actionPlanName',
                   additionalFields: '_all',
                   resolved: false,
                   s: 'FILE_LINE',
@@ -732,6 +735,62 @@ define([
 
         hideFilteredTooltip: function (e) {
           $(e.currentTarget).tooltip('destroy');
+        },
+
+        toggleFlowLocations: function (issue) {
+          if (this.locationsShowFor === issue) {
+            this.hideFlowLocations();
+          } else {
+            this.hideFlowLocations();
+            this.showFlowLocations(issue);
+          }
+        },
+
+        showFlowLocations: function (issue) {
+          this.locationsShowFor = issue;
+          var primaryLocation = {
+                msg: issue.get('message'),
+                textRange: issue.get('textRange')
+              },
+              _locations = [primaryLocation].concat(issue.get('secondaryLocations'));
+          _locations.forEach(this.showFlowLocation, this);
+        },
+
+        showFlowLocation: function (location) {
+          if (location && location.textRange) {
+            var line = location.textRange.startLine,
+                row = this.$('.source-line-code[data-line-number="' + line + '"]'),
+                renderedFlowLocation = this.renderFlowLocation(location);
+            row.append(renderedFlowLocation);
+            this.highlightFlowLocationInCode(location);
+          }
+        },
+
+        renderFlowLocation: function (location) {
+          location.msg = location.msg ? location.msg : ' ';
+          return this.flowLocationTemplate(location);
+        },
+
+        highlightFlowLocationInCode: function (location) {
+          for (var line = location.textRange.startLine; line <= location.textRange.endLine; line++) {
+            var row = this.$('.source-line-code[data-line-number="' + line + '"]');
+
+            // get location for the current line
+            var from = line === location.textRange.startLine ? location.textRange.startOffset : 0,
+                to = line === location.textRange.endLine ? location.textRange.endOffset : 999999,
+                _location = { from: from, to: to };
+
+            // mark issue location in the source code
+            var code = row.find('pre').html(),
+                newCode = highlightLocations(code, [_location], 'source-line-code-secondary-issue');
+            row.find('pre').html(newCode);
+          }
+        },
+
+        hideFlowLocations: function () {
+          this.locationsShowFor = null;
+          this.$('.source-viewer-flow-location').remove();
+          this.$('.source-line-code-secondary-issue').removeClass('source-line-code-secondary-issue');
         }
       });
 
diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-flow-location.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-flow-location.hbs
new file mode 100644 (file)
index 0000000..e11d220
--- /dev/null
@@ -0,0 +1 @@
+<div class="source-viewer-flow-location" title="{{msg}}">{{limitString msg}} </div>
index cbe29981233d47d7549c4fdc79d70b7d1c8cc492..24f587f1ea5551e5e42616611366c0d3741b6377 100644 (file)
@@ -21,7 +21,7 @@
 @import (reference) "../variables";
 @import (reference) "ui";
 
-@lineHeight: 18px;
+@source-line-height: 18px;
 @lineWithIssuesBackground: #ffeaea;
 @duplicationColor: #f3ca8e;
 
 }
 
 .source-viewer pre {
-  height: @lineHeight;
+  height: @source-line-height;
   padding: 0;
 }
 
 .source-viewer pre,
 .source-meta {
-  line-height: @lineHeight;
+  line-height: @source-line-height;
   font-family: @monoFontFamily;
   font-size: 12px;
 }
 
 .source-line-code {
+  position: relative;
   padding: 0 10px;
 
   .issue-list {
   background-position: bottom;
 }
 
+.source-line-code-secondary-issue {
+  display: inline-block;
+  background-color: @red;
+  color: #fff !important;
+}
+
 .source-meta {
   vertical-align: top;
   width: 1px;
 
 .source-line-bar {
   width: 5px;
-  height: @lineHeight;
+  height: @source-line-height;
 }
 
 .source-line-with-issues {
 .source-viewer-test-covered-lines {
   text-align: right;
 }
+
+.source-viewer-flow-location {
+  position: absolute;
+  top: 0;
+  right: 0;
+  line-height: @source-line-height - 2px;
+  margin: 1px 0;
+  padding: 0 10px;
+  background-color: @red;
+  color: #fff;
+  font-size: 12px;
+  z-index: @issue-flow-location-z-index;
+
+  &:before {
+    @arrow-size: (@source-line-height - 2px) / 2;
+    content: " ";
+    position: absolute;
+    top: 0;
+    right: 100%;
+    display: block;
+    .square(0);
+    border-top: @arrow-size solid transparent;
+    border-bottom: @arrow-size solid transparent;
+    border-right: @arrow-size solid @red;
+  }
+}
+
+.source-viewer-flow-location + .source-viewer-flow-location {
+  z-index: @issue-flow-location-z-index - 1;
+}
index ca4a8b2b49fc004e1cdbcd4740bfc5053b27596d..f0d20455d5e10b9d30ba7f9d77ee70e6ed2b9908 100644 (file)
@@ -536,6 +536,15 @@ a[class^="icon-"], a[class*=" icon-"] {
   background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB2aWV3Qm94PSIwIDAgOSAxNiIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNDE0MjE7Ij4gICAgPHBhdGggZD0iTTYuNTcxNDMsNS4xNDI4NkM2LjU3MTQzLDUuMjIwMjQgNi41NDMxNSw1LjI4NzIgNi40ODY2MSw1LjM0Mzc1QzYuNDMwMDYsNS40MDAzIDYuMzYzMSw1LjQyODU3IDYuMjg1NzEsNS40Mjg1N0M2LjIwODMzLDUuNDI4NTcgNi4xNDEzNyw1LjQwMDMgNi4wODQ4Miw1LjM0Mzc1QzYuMDI4MjcsNS4yODcyIDYsNS4yMjAyNCA2LDUuMTQyODZDNiw0Ljg2OTA1IDUuODM5MjksNC42NTc3NCA1LjUxNzg2LDQuNTA4OTNDNS4xOTY0Myw0LjM2MDEyIDQuODgwOTUsNC4yODU3MSA0LjU3MTQzLDQuMjg1NzFDNC40OTQwNSw0LjI4NTcxIDQuNDI3MDgsNC4yNTc0NCA0LjM3MDU0LDQuMjAwODlDNC4zMTM5OSw0LjE0NDM1IDQuMjg1NzEsNC4wNzczOCA0LjI4NTcxLDRDNC4yODU3MSwzLjkyMjYyIDQuMzEzOTksMy44NTU2NSA0LjM3MDU0LDMuNzk5MTFDNC40MjcwOCwzLjc0MjU2IDQuNDk0MDUsMy43MTQyOSA0LjU3MTQzLDMuNzE0MjlDNC44NjkwNSwzLjcxNDI5IDUuMTY1MTgsMy43NjE5IDUuNDU5ODIsMy44NTcxNEM1Ljc1NDQ2LDMuOTUyMzggNi4wMTMzOSw0LjExMzEgNi4yMzY2MSw0LjMzOTI5QzYuNDU5ODIsNC41NjU0OCA2LjU3MTQzLDQuODMzMzMgNi41NzE0Myw1LjE0Mjg2Wk04LDUuMTQyODZDOCw0LjcxNDI5IDcuODk3MzIsNC4zMTU0OCA3LjY5MTk2LDMuOTQ2NDNDNy40ODY2MSwzLjU3NzM4IDcuMjE4NzUsMy4yNzUzIDYuODg4MzksMy4wNDAxOEM2LjU1ODA0LDIuODA1MDYgNi4xOTE5NiwyLjYyMDU0IDUuNzkwMTgsMi40ODY2MUM1LjM4ODM5LDIuMzUyNjggNC45ODIxNCwyLjI4NTcxIDQuNTcxNDMsMi4yODU3MUM0LjE2MDcxLDIuMjg1NzEgMy43NTQ0NiwyLjM1MjY4IDMuMzUyNjgsMi40ODY2MUMyLjk1MDg5LDIuNjIwNTQgMi41ODQ4MiwyLjgwNTA2IDIuMjU0NDYsMy4wNDAxOEMxLjkyNDExLDMuMjc1MyAxLjY1NjI1LDMuNTc3MzggMS40NTA4OSwzLjk0NjQzQzEuMjQ1NTQsNC4zMTU0OCAxLjE0Mjg2LDQuNzE0MjkgMS4xNDI4Niw1LjE0Mjg2QzEuMTQyODYsNS43NDQwNSAxLjM0NTI0LDYuMjc5NzYgMS43NSw2Ljc1QzEuODA5NTIsNi44MTU0OCAxLjkwMDMsNi45MTM2OSAyLjAyMjMyLDcuMDQ0NjRDMi4xNDQzNSw3LjE3NTYgMi4yMzUxMiw3LjI3MzgxIDIuMjk0NjQsNy4zMzkyOUMzLjA1NjU1LDguMjUgMy40NzYxOSw5LjEzNjkgMy41NTM1NywxMEw1LjU4OTI5LDEwQzUuNjY2NjcsOS4xMzY5IDYuMDg2MzEsOC4yNSA2Ljg0ODIxLDcuMzM5MjlDNi45MDc3NCw3LjI3MzgxIDYuOTk4NTEsNy4xNzU2IDcuMTIwNTQsNy4wNDQ2NEM3LjI0MjU2LDYuOTEzNjkgNy4zMzMzMyw2LjgxNTQ4IDcuMzkyODYsNi43NUM3Ljc5NzYyLDYuMjc5NzYgOCw1Ljc0NDA1IDgsNS4xNDI4NlpNOS4xNDI4Niw1LjE0Mjg2QzkuMTQyODYsNi4wNjU0OCA4LjgzNjMxLDYuODYzMSA4LjIyMzIxLDcuNTM1NzFDNy45NTUzNiw3LjgyNzM4IDcuNzMzNjMsOC4wODYzMSA3LjU1ODA0LDguMzEyNUM3LjM4MjQ0LDguNTM4NjkgNy4yMDUzNiw4LjgyMjkyIDcuMDI2NzksOS4xNjUxOEM2Ljg0ODIxLDkuNTA3NDQgNi43NDcwMiw5LjgyNzM4IDYuNzIzMjEsMTAuMTI1QzcuMDAyOTgsMTAuMjkxNyA3LjE0Mjg2LDEwLjUzNTcgNy4xNDI4NiwxMC44NTcxQzcuMTQyODYsMTEuMDc3NCA3LjA2ODQ1LDExLjI2NzkgNi45MTk2NCwxMS40Mjg2QzcuMDY4NDUsMTEuNTg5MyA3LjE0Mjg2LDExLjc3OTggNy4xNDI4NiwxMkM3LjE0Mjg2LDEyLjMwOTUgNy4wMDg5MywxMi41NTA2IDYuNzQxMDcsMTIuNzIzMkM2LjgxODQ1LDEyLjg2MDEgNi44NTcxNCwxMyA2Ljg1NzE0LDEzLjE0MjlDNi44NTcxNCwxMy40MTY3IDYuNzYzMzksMTMuNjI4IDYuNTc1ODksMTMuNzc2OEM2LjM4ODM5LDEzLjkyNTYgNi4xNTc3NCwxNCA1Ljg4MzkzLDE0QzUuNzY0ODgsMTQuMjYxOSA1LjU4NjMxLDE0LjQ3MDIgNS4zNDgyMSwxNC42MjVDNS4xMTAxMiwxNC43Nzk4IDQuODUxMTksMTQuODU3MSA0LjU3MTQzLDE0Ljg1NzFDNC4yOTE2NywxNC44NTcxIDQuMDMyNzQsMTQuNzc5OCAzLjc5NDY0LDE0LjYyNUMzLjU1NjU1LDE0LjQ3MDIgMy4zNzc5OCwxNC4yNjE5IDMuMjU4OTMsMTRDMi45ODUxMiwxNCAyLjc1NDQ2LDEzLjkyNTYgMi41NjY5NiwxMy43NzY4QzIuMzc5NDYsMTMuNjI4IDIuMjg1NzEsMTMuNDE2NyAyLjI4NTcxLDEzLjE0MjlDMi4yODU3MSwxMyAyLjMyNDQsMTIuODYwMSAyLjQwMTc5LDEyLjcyMzJDMi4xMzM5MywxMi41NTA2IDIsMTIuMzA5NSAyLDEyQzIsMTEuNzc5OCAyLjA3NDQsMTEuNTg5MyAyLjIyMzIxLDExLjQyODZDMi4wNzQ0LDExLjI2NzkgMiwxMS4wNzc0IDIsMTAuODU3MUMyLDEwLjUzNTcgMi4xMzk4OCwxMC4yOTE3IDIuNDE5NjQsMTAuMTI1QzIuMzk1ODMsOS44MjczOCAyLjI5NDY0LDkuNTA3NDQgMi4xMTYwNyw5LjE2NTE4QzEuOTM3NSw4LjgyMjkyIDEuNzYwNDIsOC41Mzg2OSAxLjU4NDgyLDguMzEyNUMxLjQwOTIzLDguMDg2MzEgMS4xODc1LDcuODI3MzggMC45MTk2NDMsNy41MzU3MUMwLjMwNjU0OCw2Ljg2MzEgMCw2LjA2NTQ4IDAsNS4xNDI4NkMwLDQuNTUzNTcgMC4xMzI0NCw0LjAwNDQ2IDAuMzk3MzIxLDMuNDk1NTRDMC42NjIyMDIsMi45ODY2MSAxLjAxMDQyLDIuNTYzOTkgMS40NDE5NiwyLjIyNzY4QzEuODczNTEsMS44OTEzNyAyLjM2MTYxLDEuNjI2NDkgMi45MDYyNSwxLjQzMzA0QzMuNDUwODksMS4yMzk1OCA0LjAwNTk1LDEuMTQyODYgNC41NzE0MywxLjE0Mjg2QzUuMTM2OSwxLjE0Mjg2IDUuNjkxOTYsMS4yMzk1OCA2LjIzNjYxLDEuNDMzMDRDNi43ODEyNSwxLjYyNjQ5IDcuMjY5MzUsMS44OTEzNyA3LjcwMDg5LDIuMjI3NjhDOC4xMzI0NCwyLjU2Mzk5IDguNDgwNjUsMi45ODY2MSA4Ljc0NTU0LDMuNDk1NTRDOS4wMTA0Miw0LjAwNDQ2IDkuMTQyODYsNC41NTM1NyA5LjE0Mjg2LDUuMTQyODZaIiBzdHlsZT0iZmlsbDpyZ2IoNDUsMTM1LDE5Mik7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9zdmc+);
   background-repeat: no-repeat;
 }
+.icon-issue-flow {
+  position: relative;
+  top: 1px;
+  display: inline-block;
+  vertical-align: top;
+  .size(14px, 14px);
+  background-image: url(data:image/svg+xml,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M2.977%2012.656c0%20.417-.142.745-.426.985-.283.24-.636.36-1.058.36-.552%200-1-.172-1.344-.516l.446-.687c.255.234.53.35.828.35.15%200%20.282-.036.394-.112.112-.075.168-.186.168-.332%200-.333-.273-.48-.82-.437l-.203-.438c.043-.052.127-.165.255-.34.127-.174.238-.315.332-.422.094-.106.19-.207.29-.3v-.008c-.084%200-.21.002-.38.008-.17.005-.296.007-.38.007v.415H.25V10h2.602v.688l-.743.898c.265.062.476.19.632.383.156.19.235.42.235.686zm.015-4.898V9H.164c-.03-.188-.047-.328-.047-.422%200-.265.06-.508.184-.726.123-.22.27-.396.442-.532.172-.135.344-.26.516-.37.172-.113.32-.226.44-.34.124-.115.185-.232.185-.352%200-.13-.038-.23-.113-.3-.076-.07-.18-.106-.31-.106-.24%200-.45.15-.632.453l-.664-.46c.125-.267.31-.474.56-.622.246-.15.52-.223.823-.223.38%200%20.7.108.96.324.26.216.39.51.39.88%200%20.26-.087.498-.264.714-.177.216-.373.384-.586.504-.214.12-.41.25-.59.394-.18.144-.272.28-.277.41h.992V7.76h.82zM14%2010.25v1.5c0%20.068-.025.126-.074.176-.05.05-.108.074-.176.074h-9.5c-.068%200-.126-.025-.176-.074-.05-.05-.074-.108-.074-.176v-1.5c0-.073.023-.133.07-.18.047-.047.107-.07.18-.07h9.5c.068%200%20.126.025.176.074.05.05.074.108.074.176zM3%203.227V4H.383v-.773h.836c0-.214%200-.532.003-.954l.004-.945v-.094H1.21c-.04.09-.17.23-.39.422l-.554-.593L1.328.07h.828v3.157H3zM14%206.25v1.5c0%20.068-.025.126-.074.176-.05.05-.108.074-.176.074h-9.5c-.068%200-.126-.025-.176-.074C4.024%207.876%204%207.818%204%207.75v-1.5c0-.073.023-.133.07-.18.047-.047.107-.07.18-.07h9.5c.068%200%20.126.025.176.074.05.05.074.108.074.176zm0-4v1.5c0%20.068-.025.126-.074.176-.05.05-.108.074-.176.074h-9.5c-.068%200-.126-.025-.176-.074C4.024%203.876%204%203.818%204%203.75v-1.5c0-.068.025-.126.074-.176.05-.05.108-.074.176-.074h9.5c.068%200%20.126.025.176.074.05.05.074.108.074.176z%22%20fill%3D%22%23236A97%22%20fill-rule%3D%22nonzero%22%2F%3E%3C%2Fsvg%3E);
+  background-repeat: no-repeat;
+}
 
 
 /*
index 0f512c1ba6c7d3d69dd99a20ef4e754e88b3663c..cfa2cc22bbaaaa1f3fe63bd338f92fbcde5d88e9 100644 (file)
     }
 
   }
+
+  .issue-meta-locations {
+    position: absolute;
+    visibility: hidden;
+  }
 }
 
 .issues-workspace-list-component {
index 44a86acb13f6c6f16d54dc63ff6214e2b959353a..82366ca71be3eb7d0de6083c427612e09b51b8ea 100644 (file)
 @workspace-nav-z-index: 451;
 @workspace-viewer-z-index: 450;
 
+@issue-flow-location-z-index: 505;
+
 // ui elements
 @tooltip-z-index: 8000;
 @tip-z-index: 8000;
index 706d08687939960cb362750acacde70844a10a15..abf37c14c8c7db02e372c63bff9fd5582d333d3f 100644 (file)
       "componentId": 1875,
       "project": "com.sonarsource.samples:multiple-issue-locations",
       "subProject": "com.sonarsource.samples:multiple-issue-locations",
-      "line": 11,
+      "line": 9,
       "textRange": {
-        "startLine": 11,
-        "endLine": 11,
+        "startLine": 9,
+        "endLine": 9,
         "startOffset": 6,
-        "endOffset": 11
+        "endOffset": 10
       },
       "secondaryLocations": [
         {
           "textRange": {
-            "startLine": 10,
-            "endLine": 10,
+            "startLine": 8,
+            "endLine": 8,
             "startOffset": 6,
             "endOffset": 11
           }
index 42049ab87dbf6f2c8a2f45de3fcfff9b76608126..e17f766168116da6f573c9128e5a7d6839bae639 100644 (file)
@@ -17,7 +17,8 @@ define(['intern'], function (intern) {
     suites: [
       'test/unit/application.spec',
       'test/unit/issue.spec',
-      'test/unit/overview/card.spec'
+      'test/unit/overview/card.spec',
+      'test/unit/code-with-issue-locations-helper.spec'
     ],
 
     functionalSuites: [
index 27a5b96057f56e847f7539ebab2ed18f163ab2b5..95d33dbab18706f9c6b8c0dae87dadc301834e29 100644 (file)
@@ -19,8 +19,8 @@ define(function (require) {
             .checkElementExist('.source-line-code[data-line-number="3"] .source-line-code-issue')
             .checkElementInclude('.source-line-code[data-line-number="3"] .source-line-code-issue', '14 So')
 
-            .checkElementExist('.source-line-code[data-line-number="11"] .source-line-code-issue')
-            .checkElementInclude('.source-line-code[data-line-number="11"] .source-line-code-issue', 'arQub')
+            .checkElementExist('.source-line-code[data-line-number="9"] .source-line-code-issue')
+            .checkElementInclude('.source-line-code[data-line-number="9"] .source-line-code-issue', 'sion')
 
             .checkElementExist('.source-line-code[data-line-number="18"] .source-line-code-issue')
             .checkElementInclude('.source-line-code[data-line-number="18"] .source-line-code-issue',
@@ -28,6 +28,46 @@ define(function (require) {
             .checkElementExist('.source-line-code[data-line-number="19"] .source-line-code-issue')
             .checkElementInclude('.source-line-code[data-line-number="19"] .source-line-code-issue', ' */');
       });
+
+      bdd.it('should show secondary issue locations on the same line', function () {
+        return this.remote
+            .open()
+            .mockFromFile('/api/components/app', 'source-viewer-spec/app.json', { data: { uuid: 'uuid' } })
+            .mockFromFile('/api/sources/lines', 'source-viewer-spec/lines.json', { data: { uuid: 'uuid' } })
+            .mockFromFile('/api/issues/search',
+            'source-viewer-spec/issues-with-precise-location.json',
+            { data: { componentUuids: 'uuid' } })
+            .startApp('source-viewer', { file: file })
+            .checkElementExist('.source-line-code[data-line-number="3"] .source-line-code-issue')
+            .checkElementInclude('.source-line-code[data-line-number="3"] .source-line-code-issue', '14 So')
+            .clickElement('.source-line-with-issues[data-line-number="3"]')
+            .clickElement('.js-issue-locations')
+            .checkElementExist('.source-line-code[data-line-number="3"] .source-viewer-flow-location')
+            .checkElementCount('.source-line-code[data-line-number="3"] .source-line-code-secondary-issue', 2)
+            .checkElementInclude('.source-line-code[data-line-number="3"] .source-line-code-secondary-issue', ') 200')
+            .checkElementInclude('.source-line-code[data-line-number="3"] .source-line-code-secondary-issue', '14 So');
+      });
+
+      bdd.it('should show secondary issue locations on the different lines', function () {
+        return this.remote
+            .open()
+            .mockFromFile('/api/components/app', 'source-viewer-spec/app.json', { data: { uuid: 'uuid' } })
+            .mockFromFile('/api/sources/lines', 'source-viewer-spec/lines.json', { data: { uuid: 'uuid' } })
+            .mockFromFile('/api/issues/search',
+            'source-viewer-spec/issues-with-precise-location.json',
+            { data: { componentUuids: 'uuid' } })
+            .startApp('source-viewer', { file: file })
+            .checkElementExist('.source-line-code[data-line-number="9"] .source-line-code-issue')
+            .checkElementInclude('.source-line-code[data-line-number="9"] .source-line-code-issue', 'sion')
+            .clickElement('.source-line-with-issues[data-line-number="9"]')
+            .clickElement('.js-issue-locations')
+            .checkElementExist('.source-line-code[data-line-number="8"] .source-viewer-flow-location')
+            .checkElementExist('.source-line-code[data-line-number="9"] .source-viewer-flow-location')
+            .checkElementCount('.source-line-code[data-line-number="8"] .source-line-code-secondary-issue', 1)
+            .checkElementCount('.source-line-code[data-line-number="9"] .source-line-code-secondary-issue', 1)
+            .checkElementInclude('.source-line-code[data-line-number="8"] .source-line-code-secondary-issue', 'ense ')
+            .checkElementInclude('.source-line-code[data-line-number="9"] .source-line-code-secondary-issue', 'sion');
+      });
     });
   });
 });
diff --git a/server/sonar-web/test/unit/code-with-issue-locations-helper.spec.js b/server/sonar-web/test/unit/code-with-issue-locations-helper.spec.js
new file mode 100644 (file)
index 0000000..a1fa2cb
--- /dev/null
@@ -0,0 +1,56 @@
+define(function (require) {
+  var bdd = require('intern!bdd');
+  var assert = require('intern/chai!assert');
+
+  var helper = require('build/js/components/source-viewer/helpers/code-with-issue-locations-helper');
+
+  bdd.describe('Code With Issue Locations Helper', function () {
+    bdd.it('should exist', function () {
+      assert.equal(typeof helper, 'function');
+    });
+
+    bdd.it('should mark one location', function () {
+      var code = '<span class="k">if</span> (<span class="sym-2 sym">a</span> + <span class="c">1</span>) {',
+          locations = [{ from: 1, to: 5 }],
+          result = helper(code, locations, 'x');
+      assert.equal(result,
+          '<span class="k">i</span><span class="k x">f</span><span class=" x"> (</span><span class="sym-2 sym x">a</span><span class=""> + </span><span class="c">1</span><span class="">) {</span>');
+    });
+
+    bdd.it('should mark two locations', function () {
+      var code = 'abcdefghijklmnopqrst',
+          locations = [
+            { from: 1, to: 6 },
+            { from: 11, to: 16 }
+          ],
+          result = helper(code, locations, 'x');
+      assert.equal(result,
+          ['<span class="">a</span>',
+           '<span class=" x">bcdef</span>',
+           '<span class="">ghijk</span>',
+           '<span class=" x">lmnop</span>',
+           '<span class="">qrst</span>'].join(''));
+    });
+
+    bdd.it('should mark one locations', function () {
+      var code = '<span class="cppd"> * Copyright (C) 2008-2014 SonarSource</span>',
+          locations = [{ from: 15, to: 20 }],
+          result = helper(code, locations, 'x');
+      assert.equal(result,
+          '<span class="cppd"> * Copyright (C</span><span class="cppd x">) 200</span><span class="cppd">8-2014 SonarSource</span>');
+    });
+
+    bdd.it('should mark two locations', function () {
+      var code = '<span class="cppd"> * Copyright (C) 2008-2014 SonarSource</span>',
+          locations = [
+            { from: 24, to: 29 },
+            { from: 15, to: 20 }
+          ],
+          result = helper(code, locations, 'x');
+      assert.equal(result,
+          '<span class="cppd"> * Copyright (C</span><span class="cppd x">) 200</span><span class="cppd">8-20</span><span class="cppd x">14 So</span><span class="cppd">narSource</span>');
+      //   <span class="cppd"> * Copyright (C</span><span class="cppd x">) 200</span><span class="cppd">8-20</span><span class="cppd x">4 So</span><span class="cppd">narSource</span>
+    });
+  });
+});
+