├── .gitignore ├── .DS_Store ├── .travis.yml ├── test ├── fixtures │ ├── clone1.coffee │ ├── clone.coffee │ ├── file_2.js │ ├── file_4.js │ ├── file_3.js │ └── file_1.js └── test-jscpd.js ├── package.json ├── Gruntfile.js ├── tasks └── jscpd.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/**/*-output.xml 3 | .idea 4 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mazerte/grunt-jscpd/HEAD/.DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | before_script: 6 | - npm install -g grunt-cli -------------------------------------------------------------------------------- /test/fixtures/clone1.coffee: -------------------------------------------------------------------------------- 1 | console.log "!!!" 2 | 3 | shjs = require "shelljs" 4 | 5 | class Clone 6 | constructor: (@firstFile, @secondFile, @firstFileStart, @secondFileStart, @linesCount, @tokensCount)-> 7 | 8 | getLines: -> 9 | code = shjs.cat(@firstFile) 10 | lines = code.split '\n' 11 | start = @firstFileStart 12 | end = start + @linesCount 13 | lines[start..end].join("\n") 14 | 15 | exports.Clone = Clone 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-jscpd", 3 | "version": "0.0.12", 4 | "author": { 5 | "name": "Mathieu Desvé" 6 | }, 7 | "description": "Grunt task for use JSCPD lib", 8 | "main": "Gruntfile.js", 9 | "scripts": { 10 | "test": "grunt test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/mazerte/grunt-jscpd" 15 | }, 16 | "keywords": [ 17 | "grunt", 18 | "gruntplugin", 19 | "coffeescript", 20 | "javascript", 21 | "cpd", 22 | "copy", 23 | "paste", 24 | "detection", 25 | "pmd" 26 | ], 27 | "license": "BSD", 28 | "engines": { 29 | "node": "*" 30 | }, 31 | "dependencies": { 32 | "jscpd": "~0.3.3" 33 | }, 34 | "peerDependencies": { 35 | "grunt": ">=0.4.0" 36 | }, 37 | "devDependencies": { 38 | "grunt": "~0.4.2", 39 | "grunt-mocha-test": "~0.8.1", 40 | "chai-fs": "0.0.3", 41 | "chai": "~1.8.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/fixtures/clone.coffee: -------------------------------------------------------------------------------- 1 | shjs = require "shelljs" 2 | 3 | class Clone 4 | constructor: (@firstFile, @secondFile, @firstFileStart, @secondFileStart, @linesCount, @tokensCount)-> 5 | 6 | getLines: -> 7 | code = shjs.cat(@firstFile) 8 | lines = code.split '\n' 9 | start = @firstFileStart 10 | end = start + @linesCount 11 | lines[start..end].join("\n") 12 | 13 | exports.Clone = Clone 14 | 15 | 16 | class Map 17 | constructor: -> 18 | @clones = [] 19 | @clonesByFile = {} 20 | @numberOfDuplication = 0 21 | @numberOfLines = 0 22 | @numberOfFiles = 0 23 | 24 | addClone: (clone)-> 25 | @clones.push clone 26 | @numberOfDuplication = @numberOfDuplication + clone.linesCount 27 | 28 | if clone.firstFile of @clonesByFile 29 | @clonesByFile[clone.firstFile].push clone.firstFile 30 | else 31 | @clonesByFile[clone.firstFile] = [clone.firstFile] 32 | @numberOfFiles++ 33 | 34 | if clone.secondFile of @clonesByFile 35 | @clonesByFile[clone.secondFile].push clone 36 | else 37 | @clonesByFile[clone.secondFile] = [clone] 38 | @numberOfFiles++ 39 | 40 | getPercentage: -> 41 | result = 100 42 | if @numberOfLines > 0 43 | result = @numberOfDuplication / @numberOfLines * 100 44 | result.toFixed 2 45 | 46 | 47 | exports.Map = Map 48 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | // Load local tasks. 6 | grunt.loadTasks('tasks'); 7 | grunt.loadNpmTasks('grunt-mocha-test'); 8 | 9 | // Project configuration. 10 | grunt.initConfig({ 11 | 12 | jscpd: { 13 | javascript: { 14 | path: 'test/fixtures/', 15 | output: 'test/js-output.xml' 16 | }, 17 | coffeescript: { 18 | options: { 19 | coffee: true 20 | }, 21 | path: 'test/fixtures/', 22 | output: 'test/coffee-output.xml' 23 | }, 24 | javascriptWithExcludeString: { 25 | path: 'test/fixtures/', 26 | output: 'test/js-exclude-string-output.xml', 27 | exclude: 'file_3.js' 28 | }, 29 | javascriptWithExcludeArray: { 30 | path: 'test/fixtures/', 31 | output: 'test/js-exclude-array-output.xml', 32 | exclude: ['file_2.js', 'file_3.js'] 33 | }, 34 | javascriptWithUnexistedOutputDir: { 35 | path: 'test/fixtures/', 36 | output: 'test/unexisted/output/dir/js-output.xml' 37 | } 38 | }, 39 | 40 | mochaTest: { 41 | test: { 42 | options: { 43 | reporter: 'spec' 44 | }, 45 | src: ['test/**/test-*.js'] 46 | } 47 | } 48 | 49 | }); 50 | 51 | // Default task. 52 | grunt.registerTask('default', ['jscpd']); 53 | grunt.registerTask('test', ['jscpd', 'mochaTest']); 54 | 55 | }; 56 | -------------------------------------------------------------------------------- /test/fixtures/file_2.js: -------------------------------------------------------------------------------- 1 | module.exports = function (store) { 2 | function getset (name, value) { 3 | var node = vars.store; 4 | var keys = name.split('.'); 5 | keys.slice(0,-1).forEach(function (k) { 6 | if (node[k] === undefined) node[k] = {}; 7 | node = node[k] 8 | }); 9 | var key = keys[keys.length - 1]; 10 | if (arguments.length == 1) { 11 | return node[key]; 12 | } 13 | else { 14 | return node[key] = value; 15 | } 16 | } 17 | 18 | var vars = { 19 | get : function (name) { 20 | return getset(name); 21 | }, 22 | set : function (name, value) { 23 | return getset(name, value); 24 | }, 25 | store : store || {}, 26 | }; 27 | return vars; 28 | }; 29 | module.exports = function (store) { 30 | function getset (name, value) { 31 | var node = vars.store; 32 | var keys = name.split('.'); 33 | keys.slice(0,-1).forEach(function (k) { 34 | if (node[k] === undefined) node[k] = {}; 35 | node = node[k] 36 | }); 37 | var key = keys[keys.length - 1]; 38 | if (arguments.length == 1) { 39 | return node[key]; 40 | } 41 | else { 42 | return node[key] = value; 43 | } 44 | } 45 | 46 | var vars = { 47 | get : function (name) { 48 | return getset(name); 49 | }, 50 | set : function (name, value) { 51 | return getset(name, value); 52 | }, 53 | store : store || {}, 54 | }; 55 | return vars; 56 | }; -------------------------------------------------------------------------------- /test/fixtures/file_4.js: -------------------------------------------------------------------------------- 1 | var eldest, grade; 2 | 3 | grade = function(student) { 4 | if (student.excellentWork) { 5 | return "A+"; 6 | } else if (student.okayStuff) { 7 | if (student.triedHard) { 8 | return "B"; 9 | } else { 10 | return "B-"; 11 | } 12 | } else { 13 | return "C"; 14 | } 15 | }; 16 | 17 | eldest = 24 > 21 ? "Liz" : "Ike"; 18 | 19 | 20 | var volume, winner; 21 | 22 | if (ignition === true) { 23 | launch(); 24 | } 25 | 26 | if (band !== SpinalTap) { 27 | volume = 10; 28 | } 29 | 30 | if (answer !== false) { 31 | letTheWildRumpusBegin(); 32 | } 33 | 34 | if (car.speed < limit) { 35 | accelerate(); 36 | } 37 | 38 | if (pick === 47 || pick === 92 || pick === 13) { 39 | winner = true; 40 | } 41 | 42 | print(inspect("My name is " + this.name)); 43 | 44 | var footprints, solipsism, speed; 45 | 46 | if ((typeof mind !== "undefined" && mind !== null) && (typeof world === "undefined" || world === null)) { 47 | solipsism = true; 48 | } 49 | 50 | speed = 0; 51 | 52 | if (speed == null) { 53 | speed = 15; 54 | } 55 | 56 | footprints = typeof yeti !== "undefined" && yeti !== null ? yeti : "bear"; 57 | 58 | eldest = 24 > 21 ? "Liz" : "Ike"; 59 | 60 | 61 | var volume, winner; 62 | 63 | if (ignition === true) { 64 | launch(); 65 | } 66 | 67 | if (band !== SpinalTap) { 68 | volume = 10; 69 | } 70 | 71 | if (answer !== false) { 72 | letTheWildRumpusBegin(); 73 | } 74 | 75 | if (car.speed < limit) { 76 | accelerate(); 77 | } 78 | 79 | if (pick === 47 || pick === 92 || pick === 13) { 80 | winner = true; 81 | } 82 | 83 | print(inspect("My name is " + this.name)); -------------------------------------------------------------------------------- /test/fixtures/file_3.js: -------------------------------------------------------------------------------- 1 | function utf8_encode ( str_data ) { 2 | // Encodes an ISO-8859-1 string to UTF-8 3 | // 4 | // + original by: Webtoolkit.info (http://www.webtoolkit.info/) 5 | str_data = str_data.replace(/\r\n/g,"\n"); 6 | var utftext = ""; 7 | 8 | for (var n = 0; n < str_data.length; n++) { 9 | var c = str_data.charCodeAt(n); 10 | if (c < 128) { 11 | utftext += String.fromCharCode(c); 12 | } else if((c > 127) && (c < 2048)) { 13 | utftext += String.fromCharCode((c >> 6) | 192); 14 | utftext += String.fromCharCode((c & 63) | 128); 15 | } else { 16 | utftext += String.fromCharCode((c >> 12) | 224); 17 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 18 | utftext += String.fromCharCode((c & 63) | 128); 19 | } 20 | } 21 | 22 | return utftext; 23 | } 24 | 25 | module.exports = function (store) { 26 | function getset (name, value) { 27 | var node = vars.store; 28 | var keys = name.split('.'); 29 | keys.slice(0,-1).forEach(function (k) { 30 | if (node[k] === undefined) node[k] = {}; 31 | node = node[k] 32 | }); 33 | var key = keys[keys.length - 1]; 34 | if (arguments.length == 1) { 35 | return node[key]; 36 | } 37 | else { 38 | return node[key] = value; 39 | } 40 | } 41 | 42 | var vars = { 43 | get : function (name) { 44 | return getset(name); 45 | }, 46 | set : function (name, value) { 47 | return getset(name, value); 48 | }, 49 | store : store || {}, 50 | }; 51 | return vars; 52 | }; -------------------------------------------------------------------------------- /test/fixtures/file_1.js: -------------------------------------------------------------------------------- 1 | /** 2 | *12312 3 | */ 4 | function utf8_encode ( str_data ) { 5 | // Encodes an ISO-8859-1 string to UTF-8 6 | // 7 | // + original by: Webtoolkit.info (http://www.webtoolkit.info/) 8 | str_data = str_data.replace(/\r\n/g,"\n"); 9 | var utftext = ""; 10 | 11 | for (var n = 0; n < str_data.length; n++) { 12 | var c = str_data.charCodeAt(n); 13 | if (c < 128) { 14 | utftext += String.fromCharCode(c); 15 | } else if((c > 127) && (c < 2048)) { 16 | utftext += String.fromCharCode((c >> 6) | 192); 17 | utftext += String.fromCharCode((c & 63) | 128); 18 | } else { 19 | utftext += String.fromCharCode((c >> 12) | 224); 20 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 21 | utftext += String.fromCharCode((c & 63) | 128); 22 | } 23 | } 24 | 25 | return utftext; 26 | } 27 | 28 | module.exports = function (store) { 29 | function getset (name, value) { 30 | var node = vars.store; 31 | var keys = name.split('.'); 32 | keys.slice(0,-1).forEach(function (k) { 33 | if (node[k] === undefined) node[k] = {}; 34 | node = node[k] 35 | }); 36 | var key = keys[keys.length - 1]; 37 | if (arguments.length == 1) { 38 | return node[key]; 39 | } 40 | else { 41 | return node[key] = value; 42 | } 43 | } 44 | 45 | var vars = { 46 | get : function (name) { 47 | return getset(name); 48 | }, 49 | set : function (name, value) { 50 | return getset(name, value); 51 | }, 52 | store : store || {}, 53 | }; 54 | return vars; 55 | }; -------------------------------------------------------------------------------- /tasks/jscpd.js: -------------------------------------------------------------------------------- 1 | var jscpd = require('jscpd'); 2 | 3 | module.exports = function(grunt) { 4 | 5 | function ensureCleanPath(options) { 6 | if (typeof options.path === "string" && options.path.length > 0) { 7 | while (options.path.substr(-1) === "/") { 8 | options.path = options.path.substr(0, options.path.length - 1); 9 | } 10 | } else if (!options.path) { 11 | options.path = "."; 12 | } 13 | } 14 | 15 | function ensureOutputDir(options) { 16 | if (options.output) { 17 | options.output = grunt.template.process(options.output); 18 | var path = require("path"); 19 | var destDir = path.dirname(options.output); 20 | if (!grunt.file.exists(destDir)) { 21 | grunt.file.mkdir(destDir); 22 | } 23 | } 24 | } 25 | 26 | function failIfTooMuchDuplicateLines(threshold, resultMap) { 27 | if (threshold) { 28 | if (resultMap.numberOfDuplication > threshold) { 29 | grunt.log.error("Error: too much duplicated lines"); 30 | return false; 31 | } 32 | } 33 | } 34 | 35 | grunt.registerMultiTask('jscpd', 'Find copy/paste', function() { 36 | 37 | var options = this.options({ 38 | coffee: false 39 | }); 40 | 41 | options.path = this.data.path; 42 | options.exclude = this.data.exclude || null; 43 | options.output = this.data.output; 44 | 45 | if (this.data.exclude === undefined) { 46 | options.exclude = null; 47 | } else { 48 | options.exclude = this.data.exclude; 49 | } 50 | 51 | ensureCleanPath(options); 52 | ensureOutputDir(options); 53 | 54 | try { 55 | var instance = new jscpd(); 56 | var result = instance.run(options); 57 | return failIfTooMuchDuplicateLines(options.threshold, result.map); 58 | } catch(err) { 59 | grunt.log.error("Error: " + err.message); 60 | throw err; 61 | } 62 | 63 | }); 64 | 65 | }; 66 | -------------------------------------------------------------------------------- /test/test-jscpd.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'), 2 | expect = chai.expect, 3 | fs = require('fs'), 4 | chaiFS = require('chai-fs'); 5 | 6 | chai.use(chaiFS); 7 | 8 | describe("Grunt JSCPD", function() { 9 | 10 | describe("Javascript", function() { 11 | 12 | it("Generates non-empty file with JSCPD results", function() { 13 | 14 | expect("test/js-output.xml").to.be.a.file().and.not.empty; 15 | 16 | }); 17 | 18 | it("Generates non-empty file with JSCPD results in unexisted output dir", function () { 19 | 20 | expect("test/unexisted/output/dir/js-output.xml").to.be.a.file().and.not.empty; 21 | 22 | }); 23 | 24 | var javascriptOutput = fs.readFileSync("test/js-output.xml", 'utf8'); 25 | it("Identifies code duplication in 'file_3.js'", function() { 26 | 27 | expect(javascriptOutput).to.contain("file_3.js"); 28 | 29 | }); 30 | 31 | var javascriptOutputExcludeString = fs.readFileSync("test/js-exclude-string-output.xml", 'utf8'); 32 | it("Honours option `exclude` of type string", function() { 33 | 34 | /* 35 | * Requires true negative test above to have passed (js-output.xml needs to have file_3.js) 36 | * otherwise this test is meaningless 37 | */ 38 | 39 | // True positive Test 40 | expect(javascriptOutputExcludeString).not.to.contain("file_3.js"); 41 | }); 42 | 43 | var javascriptOutputExcludeArray = fs.readFileSync("test/js-exclude-array-output.xml", 'utf8'); 44 | it("Honours option `exclude` of type array", function() { 45 | 46 | /* 47 | * Requires true negative test above to have passed (js-output.xml needs to have file_3.js) 48 | * otherwise this test is meaningless 49 | */ 50 | 51 | // True positive Test 52 | expect(javascriptOutputExcludeArray) 53 | .not.to.contain("file_2.js") 54 | .and.not.to.contain("file_3.js"); 55 | }); 56 | 57 | }); 58 | 59 | describe("Coffeescript", function() { 60 | 61 | it("Generates non-empty file with JSCPD results", function() { 62 | 63 | expect("test/coffee-output.xml").to.be.a.file().and.not.empty; 64 | 65 | }); 66 | 67 | }); 68 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Grunt JSCPD 2 | =========== 3 | 4 | [![Build Status](https://travis-ci.org/mazerte/grunt-jscpd.png?branch=master)](https://travis-ci.org/mazerte/grunt-jscpd) 5 | [![Dependency Status](https://gemnasium.com/mazerte/grunt-jscpd.png)](https://gemnasium.com/mazerte/grunt-jscpd) 6 | [![Code Climate](https://codeclimate.com/github/mazerte/grunt-jscpd.png)](https://codeclimate.com/github/mazerte/grunt-jscpd) 7 | [![Built with Grunt](https://cdn.gruntjs.com/builtwith.png)](http://gruntjs.com/) 8 | 9 | [![NPM](https://nodei.co/npm/grunt-jscpd.png?downloads=true&stars=true)](https://nodei.co/npm/grunt-jscpd/) 10 | 11 | Grunt task for use [jscpd](https://github.com/kucherenko/jscpd/). 12 | `jscpd` is a tool for detect copy/past "design pattern" in JavaScript and CoffeeScript code. 13 | 14 | Installation 15 | ------------ 16 | 17 | ```bash 18 | npm install grunt-jscpd 19 | ``` 20 | 21 | ```javascript 22 | // Gruntfile.js 23 | grunt.loadNpmTasks('grunt-jscpd'); 24 | ``` 25 | 26 | Usage 27 | ----- 28 | 29 | Create a "jscpd" section in your Gruntfile 30 | ```javascript 31 | // Gruntfile.js 32 | grunt.initConfig({ 33 | jscpd: { 34 | javascript: { 35 | path: 'lib/js/', 36 | exclude: ['globalize/**', 'plugins/**'] 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | Example with coffee option 43 | ```coffeescript 44 | // Gruntfile.js 45 | grunt.initConfig({ 46 | jscpd: { 47 | coffeescript: { 48 | options: { 49 | coffee: true 50 | }, 51 | path: 'src/coffee/' 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | Options 58 | ------- 59 | 60 | ### Data 61 | 62 | #### path 63 | Type: `String` 64 | 65 | Path to source folder 66 | 67 | #### exclude 68 | Type: `String|Array` - optional 69 | 70 | Glob pattern for files to exclude from the analysis. 71 | 72 | #### output 73 | Type: `String` - optional 74 | 75 | Path to the output file 76 | 77 | #### exclude 78 | Type: `String` or `Array` - optional 79 | 80 | Path to directory or files to ignore 81 | 82 | ### Options 83 | 84 | #### coffee 85 | Type: `Boolean` - `default: false` 86 | 87 | Source type is in CoffeeScript language 88 | 89 | #### min-lines 90 | Type: `Number` - `default: 5` 91 | 92 | Min size of duplication in code lines to include it in report 93 | 94 | #### min-tokens 95 | Type: `Number` - `default: 70` 96 | 97 | Min size of duplication in code tokens 98 | 99 | Thanks 100 | ------ 101 | 102 | Thanks to [Andrey Kucherenko](https://github.com/kucherenko) to [jscpd](https://github.com/kucherenko/jscpd) 103 | 104 | 105 | 106 | 107 | --------------------------------------------------------------------------------