diff --git a/front/src/css/rule.css b/front/src/css/rule.css index b66c470..2a2dce0 100644 --- a/front/src/css/rule.css +++ b/front/src/css/rule.css @@ -148,3 +148,45 @@ font-style: italic; font-weight: normal; } +.colorPalette { + width: 30em; + border: 2px solid #000; + text-align: left; + /* Checkerboard background */ + background-color: #ddd; + background-image: linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa), linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa); + background-size: 1em 1em; + background-position: 0 0, 0.5em 0.5em; +} +.colorPalette > div { + display: inline-block; + height: 2em; + position: relative; +} +.colorPalette > div div { + display: none; + position: absolute; + left: 100%; + top: 100%; + background: #FFF; + padding: 0.5em; + border: 2px solid #f1c40f; + border-radius: 0.5em; + white-space: nowrap; + z-index: 3; + font-weight: bold; +} +.colorPalette > div:hover div { + display: block; +} +.colorPalette > div:hover:after { + content: " "; + position: absolute; + background-color: inherit; + left: -0.2em; + top: -0.2em; + width: 100%; + height: 100%; + z-index: 2; + border: 0.2em solid #f1c40f; +} diff --git a/front/src/less/rule.less b/front/src/less/rule.less index ccf862a..ce68297 100644 --- a/front/src/less/rule.less +++ b/front/src/less/rule.less @@ -168,4 +168,52 @@ font-weight: normal; } } +} + +.colorPalette { + width: 30em; + border: 2px solid #000; + text-align: left; + + /* Checkerboard background */ + background-color: #ddd; + background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA); + background-size:1em 1em; + background-position:0 0, 0.5em 0.5em; + + > div { + display: inline-block; + height: 2em; + position: relative; + + div { + display: none; + position: absolute; + left: 100%; + top: 100%; + background: #FFF; + padding: 0.5em; + border: 2px solid #f1c40f; + border-radius: 0.5em; + white-space: nowrap; + z-index: 3; + font-weight: bold; + } + + &:hover div { + display: block; + } + + &:hover:after { + content: " "; + position: absolute; + background-color: inherit; + left: -0.2em; + top: -0.2em; + width: 100%; + height: 100%; + z-index: 2; + border: 0.2em solid #f1c40f; + } + } } \ No newline at end of file diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 28e3c31..e954f73 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -128,7 +128,7 @@ () -
+
{{offender}}
@@ -143,6 +143,13 @@
+
+

This is the color palette, sized by total occurrences:

+
+
{{offender.color}} ({{offender.occurrences}} times)
+
+
+ diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 00e667b..92b99e0 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -273,7 +273,7 @@ var policies = { "message": "

Current latest versions of jQuery are 1.11 (with support for old IE versions) and 2.1 (without).

Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.

", "hasOffenders": false, "scoreFn": function(data) { - var differentVersions = data.toolsResults.phantomas.metrics.jQueryDifferentVersions; + var differentVersions = data.toolsResults.phantomas.metrics.jQueryVersionsLoaded; if (differentVersions === 0 || differentVersions > 1 || !data.toolsResults.phantomas.metrics.jQueryVersion) { // Not applicable @@ -322,7 +322,7 @@ var policies = { } } }, - "jQueryDifferentVersions": { + "jQueryVersionsLoaded": { "tool": "phantomas", "label": "Several versions loaded", "message": "

jQuery is a heavy library. You should never load jQuery more than once on the same page.

", @@ -410,6 +410,52 @@ var policies = { }; } }, + "cssColors": { + "tool": "phantomas", + "label": "Different colors", + "message": "

This is the number of different colors defined in CSS.

Your CSS project will be easier to maintain if you keep a small color set.

", + "isOkThreshold": 30, + "isBadThreshold": 150, + "isAbnormalThreshold": 300, + "hasOffenders": true, + "offendersTransformFn": function(offenders, ruleObject) { + var deduplicatedObj = {}; + + offenders.map(function(offender) { + var parts = /^([^ ]*) \((\d+) times\)$/.exec(offender); + + if (!parts) { + debug('cssColors offenders transform function error with "%s"', offender); + return; + } + + var color = parts[1]; + var count = parseInt(parts[2], 10); + + deduplicatedObj[color] = (deduplicatedObj[color] || 0) + count; + }); + + var deduplicatedTable = []; + for (var color in deduplicatedObj) { + deduplicatedTable.push({ + color: color, + occurrences: deduplicatedObj[color] + }); + } + + deduplicatedTable.sort(function(a, b) { + return b.occurrences - a.occurrences; + }); + + // Override rules.value + ruleObject.value = deduplicatedTable.length; + + return { + count: deduplicatedTable.length, + palette: deduplicatedTable + }; + } + }, "cssImports": { "tool": "phantomas", "label": "Uses of @import", diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index 48a81bb..044db52 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -32,7 +32,7 @@ "label": "jQuery version", "policies": { "jQueryVersion": 5, - "jQueryDifferentVersions": 0.1 + "jQueryVersionsLoaded": 0.1 } }, "cssSyntaxError": { @@ -46,7 +46,8 @@ "policies": { "cssRules": 2, "cssComplexSelectors": 2, - "cssComplexSelectorsByAttribute": 1.5 + "cssComplexSelectorsByAttribute": 1.5, + "cssColors": 0.5 } }, "badCSS": { diff --git a/lib/rulesChecker.js b/lib/rulesChecker.js index b82b7d7..bc570a7 100644 --- a/lib/rulesChecker.js +++ b/lib/rulesChecker.js @@ -65,7 +65,7 @@ var RulesChecker = function() { if (policy.offendersTransformFn) { try { - offendersObj = policy.offendersTransformFn(offenders); + offendersObj = policy.offendersTransformFn(offenders, rule); } catch(err) { debug('Error while transforming offenders for %s', metricName); debug(err); diff --git a/lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js b/lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js index d76db71..c4a962d 100644 --- a/lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js +++ b/lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js @@ -7,16 +7,17 @@ /* global document: true, window: true */ /* jshint -W030 */ -exports.version = '0.2.a'; +exports.version = '1.0.a'; exports.module = function(phantomas) { 'use strict'; phantomas.setMetric('jQueryVersion', ''); // @desc version of jQuery framework (if loaded) [string] + phantomas.setMetric('jQueryVersionsLoaded'); // @desc number of loaded jQuery "instances" (even in the same version) phantomas.setMetric('jQueryOnDOMReadyFunctions'); // @desc number of functions bound to onDOMReady event + phantomas.setMetric('jQueryWindowOnLoadFunctions'); // @desc number of functions bound to windowOnLoad event phantomas.setMetric('jQuerySizzleCalls'); // @desc number of calls to Sizzle (including those that will be resolved using querySelectorAll) - phantomas.setMetric('jQuerySizzleCallsDuplicated'); // @desc number of calls on the same Sizzle request - phantomas.setMetric('jQueryDifferentVersions'); //@desc number of different jQuery versions loaded on the page (not counting iframes) + //phantomas.setMetric('jQueryEventTriggers'); // @desc number of jQuery event triggers var jQueryFunctions = [ // DOM manipulations @@ -110,30 +111,28 @@ exports.module = function(phantomas) { phantomas.evaluate(function(jQueryFunctions) { (function(phantomas) { var jQuery; + var oldJQuery; - // TODO: create a helper - phantomas.spyGlobalVar() ? - window.__defineSetter__('jQuery', function(val) { + phantomas.spyGlobalVar('jQuery', function(jQuery) { var version; - var jQueryFn; - var oldJQuery = jQuery; - if (!val || !val.fn) { + if (!jQuery || !jQuery.fn) { phantomas.log('jQuery: unable to detect version!'); return; } - version = val.fn.jquery; - jQuery = val; - jQueryFn = val.fn; - // Older jQuery (v?.?) compatibility - if (!jQueryFn) { - jQueryFn = jQuery; + // Tag the current version of jQuery to avoid multiple reports of jQuery being loaded + // when it's actually only restored via $.noConflict(true) - see comments in #435 + if (jQuery.__phantomas === true) { + phantomas.log('jQuery: this instance has already been seen by phantomas'); + return; } + jQuery.__phantomas = true; - phantomas.log('jQuery: loaded v' + version); - phantomas.setMetric('jQueryVersion', version); + // report the version of jQuery + version = jQuery.fn.jquery; phantomas.emit('jQueryLoaded', version); - + phantomas.pushContext({ type: (oldJQuery) ? 'jQuery version change' : 'jQuery loaded', callDetails: { @@ -141,11 +140,13 @@ exports.module = function(phantomas) { }, backtrace: phantomas.getBacktrace() }); + oldJQuery = version; // jQuery.ready.promise // works for jQuery 1.8.0+ (released Aug 09 2012) - phantomas.spy(val.ready, 'promise', function(func) { + phantomas.spy(jQuery.ready, 'promise', function(func) { phantomas.incrMetric('jQueryOnDOMReadyFunctions'); + phantomas.addOffender('jQueryOnDOMReadyFunctions', phantomas.getCaller(3)); phantomas.pushContext({ type: 'jQuery - onDOMReady', @@ -160,9 +161,9 @@ exports.module = function(phantomas) { // Sizzle calls - jQuery.find // works for jQuery 1.3+ (released Jan 13 2009) - phantomas.spy(val, 'find', function(selector, context) { + phantomas.spy(jQuery, 'find', function(selector, context) { phantomas.incrMetric('jQuerySizzleCalls'); - phantomas.emit('onSizzleCall', selector + ' (context: ' + (phantomas.getDOMPath(context) || 'unknown') + ')'); + phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (phantomas.getDOMPath(context) || 'unknown')); phantomas.enterContext({ type: 'jQuery - find', @@ -183,12 +184,39 @@ exports.module = function(phantomas) { phantomas.leaveContext(moreData); }) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!'); + /*if (!jQuery.event) { + phantomas.spy(jQuery.event, 'trigger', function(ev, data, elem) { + var path = phantomas.getDOMPath(elem), + type = ev.type || ev; + + phantomas.log('Event: triggered "%s" on "%s"', type, path); + + phantomas.incrMetric('jQueryEventTriggers'); + phantomas.addOffender('jQueryEventTriggers', '"%s" on "%s"', type, path); + }); + }*/ + + // jQuery events bound to window' onLoad event (#451) + phantomas.spy(jQuery.fn, 'on', function(eventName, func) { + if ((eventName === 'load') && (this[0] === window)) { + phantomas.incrMetric('jQueryWindowOnLoadFunctions'); + phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2)); + + phantomas.pushContext({ + type: 'jQuery - windowOnLoad', + callDetails: { + arguments: [func] + }, + backtrace: phantomas.getBacktrace() + }); + } + }); // Add spys on many jQuery functions jQueryFunctions.forEach(function(functionName) { var capitalizedName = functionName.substring(0,1).toUpperCase() + functionName.substring(1); - phantomas.spy(jQueryFn, functionName, function(args) { + phantomas.spy(jQuery.fn, functionName, function(args) { // Clean args args = [].slice.call(arguments); @@ -263,42 +291,28 @@ exports.module = function(phantomas) { phantomas.leaveContext(); }) || phantomas.log('jQuery: can not track jQuery - ' + capitalizedName + ' (this version of jQuery doesn\'t support it)'); }); - - - }); - - window.__defineGetter__('jQuery', function() { - return jQuery; }); })(window.__phantomas); }, jQueryFunctions); }); - // count Sizzle calls to detect duplicated queries - var Collection = require('../../util/collection'), - sizzleCalls = new Collection(), - jQueryLoading = new Collection(); - - phantomas.on('onSizzleCall', function(request) { - sizzleCalls.push(request); + // store the last resource that was received + // try to report where given jQuery version was loaded from + phantomas.on('recv', function(entry) { + if (entry.isJS) { + lastUrl = entry.url; + } }); phantomas.on('jQueryLoaded', function(version) { - jQueryLoading.push(version); - }); + phantomas.log('jQuery: loaded v' + version); + phantomas.setMetric('jQueryVersion', version); - phantomas.on('report', function() { - sizzleCalls.sort().forEach(function(id, cnt) { - if (cnt > 1) { - phantomas.incrMetric('jQuerySizzleCallsDuplicated'); - phantomas.addOffender('jQuerySizzleCallsDuplicated', '%s: %d', id, cnt); - } - }); + // report multiple jQuery "instances" (issue #435) + phantomas.incrMetric('jQueryVersionsLoaded'); + phantomas.addOffender('jQueryVersionsLoaded', 'v%s', version); - jQueryLoading.forEach(function(version) { - phantomas.incrMetric('jQueryDifferentVersions'); - phantomas.addOffender('jQueryDifferentVersions', '%s', version); - }); + phantomas.log('jQuery: v%s (probably loaded from <%s>)', version, lastUrl); }); }; diff --git a/test/core/customPoliciesTest.js b/test/core/customPoliciesTest.js index 302b1c4..abfb601 100644 --- a/test/core/customPoliciesTest.js +++ b/test/core/customPoliciesTest.js @@ -404,20 +404,20 @@ describe('rulesChecker', function() { results.should.deep.equals({}); - // If jQueryDifferentVersions is 0 + // If jQueryVersionsLoaded is 0 results = rulesChecker.check({ "toolsResults": { "phantomas": { "metrics": { "jQueryVersion": "1.6.0", - "jQueryDifferentVersions": 0 + "jQueryVersionsLoaded": 0 } } } }, policies); results.should.not.have.a.property('jQueryVersion'); - results.should.have.a.property('jQueryDifferentVersions'); - results.jQueryDifferentVersions.should.have.a.property('score').that.equals(100); + results.should.have.a.property('jQueryVersionsLoaded'); + results.jQueryVersionsLoaded.should.have.a.property('score').that.equals(100); // If there are more than 1 jQuery version @@ -426,15 +426,15 @@ describe('rulesChecker', function() { "phantomas": { "metrics": { "jQueryVersion": "1.6.0", - "jQueryDifferentVersions": 2 + "jQueryVersionsLoaded": 2 } } } }, policies); results.should.not.have.a.property('jQueryVersion'); - results.should.have.a.property('jQueryDifferentVersions'); - results.jQueryDifferentVersions.should.have.a.property('score').that.equals(0); - results.jQueryDifferentVersions.should.have.a.property('abnormal').that.equals(true); + results.should.have.a.property('jQueryVersionsLoaded'); + results.jQueryVersionsLoaded.should.have.a.property('score').that.equals(0); + results.jQueryVersionsLoaded.should.have.a.property('abnormal').that.equals(true); });