| Line | Hits | Source |
|---|---|---|
| 1 | ||
| 2 | 1 | shjs = require 'shelljs' |
| 3 | 1 | Blamer = require 'blamer' |
| 4 | 1 | Promise = require 'bluebird' |
| 5 | ||
| 6 | 1 | class Clone |
| 7 | constructor: ( | |
| 8 | @firstFile, | |
| 9 | @secondFile, | |
| 10 | @firstFileStart, | |
| 11 | @secondFileStart, | |
| 12 | @linesCount, | |
| 13 | @tokensCount)-> | |
| 14 | 60 | @firstFileAnnotatedCode = {} |
| 15 | 60 | @secondFileAnnotatedCode = {} |
| 16 | ||
| 17 | ||
| 18 | getLines: (isFirstFile = yes) -> | |
| 19 | 120 | code = shjs.cat(if isFirstFile then @firstFile else @secondFile) |
| 20 | 120 | start = if isFirstFile then @firstFileStart else @secondFileStart |
| 21 | 120 | lines = code.split '\n' |
| 22 | 120 | end = start + @linesCount |
| 23 | 120 | lines[start..end].join("\n") |
| 24 | ||
| 25 | blame: -> | |
| 26 | 30 | blamer = new Blamer |
| 27 | 30 | Promise.all([ |
| 28 | blamer.blameByFile(@firstFile) | |
| 29 | blamer.blameByFile(@secondFile) | |
| 30 | ]).then (results) => | |
| 31 | 30 | @firstFileAnnotatedCode[line] = annotation for line, annotation of results[0][@firstFile] when @lineInRange(line, @firstFileStart) |
| 32 | 30 | @secondFileAnnotatedCode[line] = annotation for line, annotation of results[1][@secondFile] when @lineInRange(line, @secondFileStart) |
| 33 | 30 | return @ |
| 34 | ||
| 35 | 5255 | lineInRange: (line, fileStart) -> 0 + line >= fileStart and 0 + line <= fileStart + @linesCount |
| 36 | ||
| 37 | 1 | exports.Clone = Clone |
| 38 |
| Line | Hits | Source |
|---|---|---|
| 1 | ||
| 2 | 1 | Map = require('./map').Map |
| 3 | ||
| 4 | 1 | class Detector |
| 5 | ||
| 6 | constructor: (@strategy) -> | |
| 7 | ||
| 8 | start: (files = [], minLines = 5, minTokens = 70)-> | |
| 9 | 47 | map = new Map() |
| 10 | 47 | @strategy.detect map, file, minLines, minTokens for file in files |
| 11 | 47 | map |
| 12 | ||
| 13 | 1 | exports.Detector = Detector |
| 14 |
| Line | Hits | Source |
|---|---|---|
| 1 | 1 | logger = require 'winston' |
| 2 | ||
| 3 | 1 | {Detector} = require './detector' |
| 4 | 1 | {Strategy} = require './strategy' |
| 5 | 1 | {Report} = require './report' |
| 6 | ||
| 7 | 1 | optionsPreprocessor = require './preprocessors/options' |
| 8 | 1 | filesPreprocessor = require './preprocessors/files' |
| 9 | 1 | debugPreprocessor = require './preprocessors/debug' |
| 10 | ||
| 11 | 1 | Promise = require "bluebird" |
| 12 | ||
| 13 | 1 | class jscpd |
| 14 | ||
| 15 | preProcessors: [ | |
| 16 | optionsPreprocessor | |
| 17 | filesPreprocessor | |
| 18 | debugPreprocessor | |
| 19 | ] | |
| 20 | ||
| 21 | LANGUAGES: [] | |
| 22 | ||
| 23 | execPreProcessors: (list) -> | |
| 24 | 49 | logger.profile 'Preprocessors running time:' |
| 25 | 49 | preProcessor @ for preProcessor in list |
| 26 | 49 | logger.profile 'Preprocessors running time:' |
| 27 | ||
| 28 | run: (options) -> | |
| 29 | 48 | @options = options |
| 30 | 48 | @execPreProcessors @preProcessors |
| 31 | ||
| 32 | 48 | unless @options.debug |
| 33 | 47 | logger.profile 'Scanning for duplicates time:' |
| 34 | 47 | logger.info "Scanning #{@options.selectedFiles.length} files for duplicates..." if @options.selectedFiles.length |
| 35 | ||
| 36 | 47 | strategy = new Strategy @options |
| 37 | 47 | detector = new Detector strategy |
| 38 | ||
| 39 | 47 | report = new Report @options |
| 40 | ||
| 41 | 47 | codeMap = detector.start @options.selectedFiles, @options['min-lines'], @options['min-tokens'] |
| 42 | ||
| 43 | 47 | logger.profile 'Scanning for duplicates time:' |
| 44 | 47 | logger.info 'Scanning... done!\n' |
| 45 | ||
| 46 | 47 | logger.profile 'Generate report time:' |
| 47 | 47 | logger.info 'Start report generation...\n' |
| 48 | ||
| 49 | 47 | generateReport = => |
| 50 | 47 | reportResult = report.generate codeMap |
| 51 | 47 | logger.profile 'Generate report time:' |
| 52 | 47 | @report = reportResult |
| 53 | 47 | @map = codeMap |
| 54 | 47 | report: @report, map: @map |
| 55 | ||
| 56 | 47 | if @options.blame |
| 57 | 16 | Promise |
| 58 | 30 | .all(clone.blame() for clone in codeMap.clones) |
| 59 | .then generateReport | |
| 60 | .error generateReport | |
| 61 | else | |
| 62 | 31 | return generateReport() |
| 63 | ||
| 64 | 1 | module.exports = jscpd |
| 65 |
| Line | Hits | Source |
|---|---|---|
| 1 | ||
| 2 | 1 | class Map |
| 3 | constructor: -> | |
| 4 | 47 | @clones = [] |
| 5 | 47 | @clonesByFile = {} |
| 6 | 47 | @numberOfDuplication = 0 |
| 7 | 47 | @numberOfLines = 0 |
| 8 | 47 | @numberOfFiles = 0 |
| 9 | ||
| 10 | addClone: (clone)-> | |
| 11 | 60 | @clones.push clone |
| 12 | 60 | @numberOfDuplication = @numberOfDuplication + clone.linesCount |
| 13 | ||
| 14 | 60 | if clone.firstFile of @clonesByFile |
| 15 | 26 | @clonesByFile[clone.firstFile].push clone.firstFile |
| 16 | else | |
| 17 | 34 | @clonesByFile[clone.firstFile] = [clone.firstFile] |
| 18 | 34 | @numberOfFiles++ |
| 19 | ||
| 20 | 60 | if clone.secondFile of @clonesByFile |
| 21 | 20 | @clonesByFile[clone.secondFile].push clone |
| 22 | else | |
| 23 | 40 | @clonesByFile[clone.secondFile] = [clone] |
| 24 | 40 | @numberOfFiles++ |
| 25 | ||
| 26 | getPercentage: -> | |
| 27 | 60 | result = 0 |
| 28 | 60 | if @numberOfLines > 0 |
| 29 | 30 | result = @numberOfDuplication / @numberOfLines * 100 |
| 30 | 60 | result.toFixed 2 |
| 31 | ||
| 32 | ||
| 33 | 1 | exports.Map = Map |
| 34 |
| Line | Hits | Source |
|---|---|---|
| 1 | 2 | logger = require "winston" |
| 2 | ||
| 3 | ||
| 4 | 2 | debugPreprocessor = (jscpd) -> |
| 5 | 49 | if jscpd.options.debug |
| 6 | 2 | logger.info '----------------------------------------' |
| 7 | 2 | logger.info 'Options:' |
| 8 | 2 | logger.info "#{name} = #{option}" for name, option of jscpd.options when name isnt 'selectedFiles' |
| 9 | 2 | logger.info '----------------------------------------' |
| 10 | 2 | logger.info 'Files:' |
| 11 | 2 | logger.info file for file in jscpd.options.selectedFiles |
| 12 | 2 | logger.info '----------------------------------------' |
| 13 | 2 | logger.info 'Run without debug option for start detection process' |
| 14 | ||
| 15 | ||
| 16 | 2 | module.exports = debugPreprocessor |
| 17 | ||
| 18 |
| Line | Hits | Source |
|---|---|---|
| 1 | 5 | _ = require "underscore" |
| 2 | 5 | path = require "path" |
| 3 | 5 | glob = require "glob" |
| 4 | ||
| 5 | 5 | findFiles = (jscpd) -> |
| 6 | 52 | files = [] |
| 7 | 52 | excluded_files = [] |
| 8 | ||
| 9 | 52 | for pattern in jscpd.options.patterns |
| 10 | 52 | files = _.union files, glob.sync(pattern, cwd: jscpd.options.path) |
| 11 | ||
| 12 | 52 | if jscpd.options.exclude and jscpd.options.exclude.length > 0 |
| 13 | 22 | for pattern in jscpd.options.exclude |
| 14 | 40 | excluded_files = _.union excluded_files, glob.sync(pattern, cwd: jscpd.options.path) |
| 15 | ||
| 16 | 52 | files = _.difference files, excluded_files |
| 17 | 52 | files = _.map files, (file) -> path.normalize "#{jscpd.options.path}/#{file}" |
| 18 | 52 | return files |
| 19 | ||
| 20 | 5 | prepareOptions = (jscpd) -> |
| 21 | 52 | if jscpd.options.files is null |
| 22 | 2 | jscpd.options.patterns = ["**/*.+(#{jscpd.options.extensions.join '|'})"] |
| 23 | else | |
| 24 | 50 | unless Array.isArray(jscpd.options.files) |
| 25 | 31 | jscpd.options.patterns = [jscpd.options.files] |
| 26 | else | |
| 27 | 19 | jscpd.options.patterns = jscpd.options.files |
| 28 | 52 | if jscpd.options.exclude isnt null |
| 29 | 52 | unless Array.isArray(jscpd.options.exclude) |
| 30 | 4 | jscpd.options.exclude = [jscpd.options.exclude] |
| 31 | ||
| 32 | ||
| 33 | 5 | filesPreprocessor = (jscpd) -> |
| 34 | 52 | prepareOptions jscpd |
| 35 | 52 | jscpd.options.selectedFiles = findFiles jscpd |
| 36 | ||
| 37 | 5 | module.exports = filesPreprocessor |
| 38 | ||
| 39 |
| Line | Hits | Source |
|---|---|---|
| 1 | 4 | _ = require "underscore" |
| 2 | 4 | yaml = require 'js-yaml' |
| 3 | 4 | fs = require 'fs' |
| 4 | 4 | path = require 'path' |
| 5 | ||
| 6 | 4 | TokenizerFactory = require '../tokenizer/TokenizerFactory' |
| 7 | ||
| 8 | 4 | prepareOptions = (options, config) -> |
| 9 | 51 | optionsNew = _.extend optionsPreprocessor.default, config |
| 10 | ||
| 11 | 51 | for key, value of options |
| 12 | 297 | if not (value is null) |
| 13 | 297 | optionsNew[key] = value |
| 14 | ||
| 15 | 51 | if typeof optionsNew.languages is 'string' |
| 16 | 1 | optionsNew.languages = optionsNew.languages.split ',' |
| 17 | ||
| 18 | 51 | optionsNew.extensions = TokenizerFactory::getExtensionsByLanguages(optionsNew.languages) |
| 19 | 51 | return optionsNew |
| 20 | ||
| 21 | 4 | readConfig = (file) -> |
| 22 | 52 | file = path.normalize file |
| 23 | 52 | try |
| 24 | 52 | doc = yaml.safeLoad fs.readFileSync(file, 'utf8') |
| 25 | 50 | doc.config_file = file |
| 26 | 50 | return doc |
| 27 | catch error | |
| 28 | 2 | return false |
| 29 | ||
| 30 | ||
| 31 | 4 | optionsPreprocessor = (jscpd) -> |
| 32 | 51 | config = readConfig('.cpd.yaml') or readConfig('.cpd.yml') or {} |
| 33 | 51 | options = prepareOptions jscpd.options, config |
| 34 | 51 | options.path = options.path or process.cwd() |
| 35 | 51 | jscpd.options = options |
| 36 | ||
| 37 | 4 | optionsPreprocessor.default = |
| 38 | languages: Object.keys TokenizerFactory::LANGUAGES | |
| 39 | verbose: off | |
| 40 | debug: off | |
| 41 | files: null | |
| 42 | exclude: null | |
| 43 | ||
| 44 | 4 | module.exports = optionsPreprocessor |
| 45 | ||
| 46 |
| Line | Hits | Source |
|---|---|---|
| 1 | 1 | fs = require 'fs' |
| 2 | 1 | logger = require 'winston' |
| 3 | 1 | path = require 'path' |
| 4 | ||
| 5 | ||
| 6 | ||
| 7 | 1 | class Report |
| 8 | ||
| 9 | constructor: (@options) -> | |
| 10 | ||
| 11 | 47 | reporter = @options.reporter |
| 12 | ||
| 13 | 47 | ext = @options.output.split('.').pop() if @options.output |
| 14 | ||
| 15 | 47 | if ext is 'xml' and reporter is 'json' or |
| 16 | ext is 'json' and reporter is 'xml' | |
| 17 | ||
| 18 | 0 | logger.warn "output file extention '#{@options.output}' |
| 19 | 0 | does not match reporter '#{reporter}'" |
| 20 | ||
| 21 | 47 | switch reporter |
| 22 | 30 | when 'xml' then reporter = './reporters/xml-pmd' |
| 23 | 15 | when 'json' then reporter = './reporters/json' |
| 24 | else | |
| 25 | 2 | cwd = process.cwd() |
| 26 | 2 | reporter = path.normalize reporter |
| 27 | 2 | isAbsolute = reporter.indexOf(cwd) is 0 |
| 28 | 2 | reporter = path.join(cwd, reporter) unless isAbsolute |
| 29 | ||
| 30 | 47 | @reporter = require reporter |
| 31 | 47 | @stdReporter = require './reporters/_std-log' |
| 32 | ||
| 33 | generate: (@map) -> | |
| 34 | 47 | [raw, dump, log] = @reporter @options |
| 35 | 47 | log = @stdReporter() unless log |
| 36 | ||
| 37 | 47 | logger.info log |
| 38 | 47 | if @options.output |
| 39 | 0 | fs.writeFileSync(@options.output, dump or raw) |
| 40 | else | |
| 41 | 47 | logger.warn 'output file is not provided' |
| 42 | ||
| 43 | 47 | return raw or dump |
| 44 | ||
| 45 | 1 | exports.Report = Report |
| 46 |
| Line | Hits | Source |
|---|---|---|
| 1 | 1 | require 'colors' |
| 2 | 1 | Table = require 'cli-table' |
| 3 | ||
| 4 | 1 | TABLE_CONFIGURATION = chars: |
| 5 | 'mid': '' | |
| 6 | 'left-mid': '' | |
| 7 | 'mid-mid': '' | |
| 8 | 'right-mid': '' | |
| 9 | 'top': '' | |
| 10 | 'top-mid': '' | |
| 11 | 'top-left': '' | |
| 12 | 'top-right': '' | |
| 13 | 'bottom': '' | |
| 14 | 'bottom-mid': '' | |
| 15 | 'bottom-left': '' | |
| 16 | 'bottom-right': '' | |
| 17 | 'left': '' | |
| 18 | 'right': '' | |
| 19 | 1 | compareDates = (firstDate, secondDate)-> |
| 20 | 0 | firstDate = new Date firstDate |
| 21 | 0 | secondDate = new Date secondDate |
| 22 | 0 | switch yes |
| 23 | 0 | when firstDate < secondDate then "=>" |
| 24 | 0 | when firstDate > secondDate then "<=" |
| 25 | 0 | else "==" |
| 26 | ||
| 27 | 1 | module.exports = -> |
| 28 | ||
| 29 | 45 | clog = '' |
| 30 | 45 | verbose = @options.verbose |
| 31 | ||
| 32 | 45 | for clone in @map.clones |
| 33 | 60 | do (clone) -> |
| 34 | 60 | table = '' |
| 35 | 60 | firstFile = clone.firstFile |
| 36 | 60 | secondFile = clone.secondFile |
| 37 | ||
| 38 | 60 | if verbose |
| 39 | 0 | table = new Table TABLE_CONFIGURATION |
| 40 | ||
| 41 | 0 | fragment = clone.getLines().split("\n").reduce (tbl, current, lineNumber) -> |
| 42 | 0 | firstFileLine = clone.firstFileStart + lineNumber |
| 43 | 0 | secondFileLine = clone.secondFileStart + lineNumber |
| 44 | 0 | if Object.keys(clone.firstFileAnnotatedCode) > 0 |
| 45 | 0 | tbl.push [ |
| 46 | firstFileLine | |
| 47 | clone.firstFileAnnotatedCode[firstFileLine].author | |
| 48 | compareDates clone.firstFileAnnotatedCode[firstFileLine].date, clone.secondFileAnnotatedCode[secondFileLine].date | |
| 49 | secondFileLine | |
| 50 | clone.secondFileAnnotatedCode[secondFileLine].author | |
| 51 | current.dim | |
| 52 | ] | |
| 53 | else | |
| 54 | 0 | tbl.push [ |
| 55 | firstFileLine | |
| 56 | secondFileLine | |
| 57 | current.dim | |
| 58 | ] | |
| 59 | 0 | tbl |
| 60 | , table | |
| 61 | ||
| 62 | 60 | clog = "#{clog}\n\t- |
| 63 | 60 | #{firstFile.green.bold}: |
| 64 | 60 | #{clone.firstFileStart}-#{clone.firstFileStart + clone.linesCount}\n\t |
| 65 | 60 | #{secondFile.green.bold}: |
| 66 | 60 | #{clone.secondFileStart}-#{clone.secondFileStart + clone.linesCount}\n" |
| 67 | ||
| 68 | 60 | clog = "#{clog}\n#{fragment.toString()}\n" if verbose |
| 69 | ||
| 70 | 45 | percent = @map.getPercentage() |
| 71 | ||
| 72 | 45 | log = "Found #{@map.clones.length} exact clones with |
| 73 | 45 | #{@map.numberOfDuplication} duplicated lines in |
| 74 | 45 | #{@map.numberOfFiles} files\n #{clog}\n\n |
| 75 | 45 | #{percent}% (#{@map.numberOfDuplication} lines) |
| 76 | duplicated lines out of | |
| 77 | 45 | #{@map.numberOfLines} total lines of code.\n" |
| 78 | ||
| 79 | 45 | if @options.limit <= percent |
| 80 | 0 | console.error log |
| 81 | 0 | console.error "ERROR: jscpd found too many duplicates over threshold" |
| 82 | 0 | process.exit(1) |
| 83 | ||
| 84 | 45 | return log |
| 85 |
| Line | Hits | Source |
|---|---|---|
| 1 | 1 | _ = require 'underscore' |
| 2 | ||
| 3 | # @map {Object} report object | |
| 4 | # @options {Object} Report class options | |
| 5 | 1 | module.exports = -> |
| 6 | ||
| 7 | 15 | duplicates = [] |
| 8 | ||
| 9 | 15 | for clone in @map.clones |
| 10 | 0 | do (clone) -> |
| 11 | ||
| 12 | 0 | duplicates.push |
| 13 | ||
| 14 | lines: clone.linesCount | |
| 15 | tokens: clone.tokensCount | |
| 16 | firstFile: | |
| 17 | start: clone.firstFileStart | |
| 18 | name: clone.firstFile | |
| 19 | secondFile: | |
| 20 | start: clone.secondFileStart | |
| 21 | name: clone.secondFile | |
| 22 | fragment: _.escape(clone.getLines()) | |
| 23 | ||
| 24 | 15 | json = |
| 25 | duplicates: duplicates | |
| 26 | statistics: | |
| 27 | clones: @map.clones.length | |
| 28 | duplications: @map.numberOfDuplication | |
| 29 | files: @map.numberOfFiles | |
| 30 | percentage: @map.getPercentage() | |
| 31 | lines: @map.numberOfLines | |
| 32 | ||
| 33 | 15 | [ |
| 34 | json | |
| 35 | JSON.stringify(json) | |
| 36 | ] | |
| 37 |
| Line | Hits | Source |
|---|---|---|
| 1 | 1 | _ = require 'underscore' |
| 2 | 1 | Promise = require 'bluebird' |
| 3 | ||
| 4 | # @map {Object} report object | |
| 5 | # @options {Object} Report class options | |
| 6 | 1 | module.exports = (options) -> |
| 7 | 30 | xmlDoc = "<?xml version='1.0' encoding='UTF-8' ?>" |
| 8 | 30 | if options['xsl-href'] then xmlDoc += '<?xml-stylesheet type="text/xsl" href="' + options['xsl-href'] + '"?>' |
| 9 | 30 | xmlDoc += '<pmd-cpd>' |
| 10 | ||
| 11 | # Promise.all(@map.clones.map (clone) -> clone.blame()).then (clones) -> | |
| 12 | # console.log clones | |
| 13 | ||
| 14 | 30 | for clone in @map.clones |
| 15 | 60 | do (clone) -> |
| 16 | 60 | firstFragment = _.escape clone.getLines(yes) |
| 17 | 60 | secondFragment = _.escape clone.getLines(no) |
| 18 | 60 | xmlDoc = "#{xmlDoc} |
| 19 | 60 | <duplication lines='#{clone.linesCount}' tokens='#{clone.tokensCount}'> |
| 20 | 60 | <file path='#{clone.firstFile}' line='#{clone.firstFileStart}'>#{clone.firstFileAnnotatedCode} |
| 21 | 60 | <codefragment><![CDATA[#{firstFragment}]]></codefragment> |
| 22 | </file> | |
| 23 | 60 | <file path='#{clone.secondFile}' line='#{clone.secondFileStart}'>#{clone.secondFileAnnotatedCode} |
| 24 | 60 | <codefragment><![CDATA[#{secondFragment}]]></codefragment> |
| 25 | </file> | |
| 26 | 60 | <codefragment><![CDATA[#{firstFragment}]]></codefragment> |
| 27 | </duplication>" | |
| 28 | 30 | xmlDoc = xmlDoc + '</pmd-cpd>' |
| 29 | ||
| 30 | 30 | [xmlDoc] |
| 31 |
| Line | Hits | Source |
|---|---|---|
| 1 | ||
| 2 | 1 | class StorageMemory |
| 3 | ||
| 4 | constructor: -> | |
| 5 | 47 | @codeHashes = {} |
| 6 | ||
| 7 | addHash: (hash, file, line, language)-> | |
| 8 | 11030 | @codeHashes[language] = @codeHashes[language] ? {} |
| 9 | 11030 | @codeHashes[language][hash] = line: line, file: file |
| 10 | ||
| 11 | hasHash: (hash, language) -> | |
| 12 | 20528 | @codeHashes[language] and hash of @codeHashes[language] |
| 13 | ||
| 14 | getHash: (hash, language) -> | |
| 15 | 60 | @codeHashes[language]?[hash] |
| 16 | ||
| 17 | ||
| 18 | 1 | module.exports = StorageMemory |
| Line | Hits | Source |
|---|---|---|
| 1 | 1 | shjs = require 'shelljs' |
| 2 | 1 | TokenizerFactory = require './tokenizer/TokenizerFactory' |
| 3 | 1 | crypto = require 'crypto' |
| 4 | ||
| 5 | 1 | Storage = require './storage/StorageMemory' |
| 6 | ||
| 7 | 1 | Clone = require('./clone').Clone |
| 8 | ||
| 9 | 1 | class Strategy |
| 10 | ||
| 11 | constructor: (options) -> | |
| 12 | 47 | @options = options |
| 13 | 47 | @languages = options.languages |
| 14 | 47 | @storage = new Storage() |
| 15 | ||
| 16 | detect: (map, file, @minLines, @minTokens) -> | |
| 17 | 1170 | tokenizer = TokenizerFactory::makeTokenizer file, @languages |
| 18 | 1170 | unless tokenizer |
| 19 | 1092 | return no |
| 20 | 78 | language = tokenizer.getType() |
| 21 | ||
| 22 | 78 | if shjs.test '-f', file |
| 23 | 78 | code = shjs.cat file |
| 24 | else | |
| 25 | 0 | return no |
| 26 | ||
| 27 | 78 | lines = code.split '\n' |
| 28 | 78 | map.numberOfLines = map.numberOfLines + lines.length |
| 29 | ||
| 30 | 78 | tokenizer.skipComments = @options['skip-comments'] |
| 31 | ||
| 32 | 78 | {tokensPositions, currentMap} = tokenizer.tokenize(code).generateMap() |
| 33 | ||
| 34 | 78 | firstLine = 0 |
| 35 | 78 | tokenNumber = 0 |
| 36 | 78 | isClone = false |
| 37 | ||
| 38 | 78 | while tokenNumber <= tokensPositions.length - @minTokens |
| 39 | 20528 | mapFrame = currentMap.substring tokenNumber * 33, tokenNumber * 33 + @minTokens * 33 |
| 40 | 20528 | hash = crypto.createHash('md5').update(mapFrame).digest('hex') |
| 41 | ||
| 42 | 20528 | if @storage.hasHash hash, language |
| 43 | 9498 | isClone = true |
| 44 | 9498 | if firstLine is 0 |
| 45 | 60 | firstLine = tokensPositions[tokenNumber] |
| 46 | 60 | firstHash = hash |
| 47 | 60 | firstToken = tokenNumber |
| 48 | else | |
| 49 | 11030 | if isClone |
| 50 | 28 | lastToken = tokenNumber + @minTokens - 2 |
| 51 | 28 | @addClone( |
| 52 | map, | |
| 53 | file, | |
| 54 | firstHash, | |
| 55 | firstToken, | |
| 56 | lastToken, | |
| 57 | firstLine, | |
| 58 | tokensPositions[lastToken], | |
| 59 | language | |
| 60 | ) | |
| 61 | 28 | firstLine = 0 |
| 62 | 28 | isClone = false |
| 63 | 11030 | @storage.addHash hash, file, tokensPositions[tokenNumber], language |
| 64 | 20528 | tokenNumber = tokenNumber + 1 |
| 65 | ||
| 66 | 78 | if isClone |
| 67 | 32 | lastToken = tokenNumber + @minTokens - 2 |
| 68 | 32 | @addClone( |
| 69 | map, | |
| 70 | file, | |
| 71 | firstHash, | |
| 72 | firstToken, | |
| 73 | lastToken, | |
| 74 | firstLine, | |
| 75 | tokensPositions[lastToken], | |
| 76 | language | |
| 77 | ) | |
| 78 | 32 | isClone = false |
| 79 | ||
| 80 | addClone: (map, file, hash, firstToken, lastToken, firstLine, lastLine, language) -> | |
| 81 | 60 | hashInfo = @storage.getHash(hash, language) |
| 82 | 60 | fileA = hashInfo.file |
| 83 | 60 | firstLineA = hashInfo.line |
| 84 | 60 | numLines = lastLine + 1 - firstLine |
| 85 | 60 | if numLines >= @minLines and (fileA isnt file or firstLineA isnt firstLine) |
| 86 | 60 | map.addClone new Clone( |
| 87 | fileA, | |
| 88 | file, | |
| 89 | firstLineA, | |
| 90 | firstLine, | |
| 91 | numLines, | |
| 92 | lastToken - firstToken + 1 | |
| 93 | ) | |
| 94 | ||
| 95 | ||
| 96 | 1 | exports.Strategy = Strategy |
| 97 |
| Line | Hits | Source |
|---|---|---|
| 1 | ||
| 2 | 1 | crypto = require 'crypto' |
| 3 | ||
| 4 | 1 | class TokenizerBase |
| 5 | ||
| 6 | constructor: -> | |
| 7 | 15 | @skipComments = no |
| 8 | 15 | @tokenTypes = [] |
| 9 | ||
| 10 | tokenize: (code) -> | |
| 11 | ||
| 12 | getType: () -> | |
| 13 | ||
| 14 | generateMap: () -> | |
| 15 | ||
| 16 | 35804 | isEmptyToken: (value) -> value.replace(/^\s+|\s+$/g, '').length is 0 |
| 17 | ||
| 18 | validToken: (type) -> | |
| 19 | 35804 | (@type in ['coffeescript', 'python', 'ruby'] or type isnt 'empty') and (not @skipComments or type isnt 'comment') |
| 20 | ||
| 21 | getTokenTypeId: (name) -> | |
| 22 | 25826 | result = 0 |
| 23 | 25826 | if name in @tokenTypes |
| 24 | 25689 | result = @tokenTypes.indexOf(name) |
| 25 | else | |
| 26 | 137 | result = @tokenTypes.length |
| 27 | 137 | @tokenTypes.push name |
| 28 | 25826 | result |
| 29 | ||
| 30 | createMap: (type, value) -> | |
| 31 | 25826 | String.fromCharCode(@getTokenTypeId(type)) + |
| 32 | crypto | |
| 33 | .createHash('md5') | |
| 34 | .update(value.toString()) | |
| 35 | .digest('hex') | |
| 36 | ||
| 37 | ||
| 38 | 1 | module.exports = TokenizerBase |
| 39 |
| Line | Hits | Source |
|---|---|---|
| 1 | 1 | vm = require "vm" |
| 2 | 1 | fs = require "fs" |
| 3 | ||
| 4 | 1 | TokenizerBase = require './TokenizerBase' |
| 5 | 1 | logger = require 'winston' |
| 6 | 1 | CodeMirror = require("codemirror/addon/runmode/runmode.node.js") |
| 7 | ||
| 8 | 1 | CodeMirror.loadMode = (name) -> |
| 9 | 80 | filename = require.resolve("codemirror/mode/" + name + "/" + name + ".js") |
| 10 | 72 | modeDef = null |
| 11 | 72 | try |
| 12 | 72 | modeDef = fs.readFileSync(filename, "utf8") |
| 13 | catch err | |
| 14 | 0 | throw new Error(name + " mode isn't shipped with CodeMirror") |
| 15 | 72 | vm.runInNewContext modeDef, |
| 16 | CodeMirror: CodeMirror | |
| 17 | ||
| 18 | 1 | CodeMirror.loadMode 'xml' |
| 19 | 1 | CodeMirror.loadMode 'clike' |
| 20 | ||
| 21 | ||
| 22 | 1 | class TokenizerCodeMirror extends TokenizerBase |
| 23 | 1 | @type = null |
| 24 | ||
| 25 | setType: (type) -> | |
| 26 | 15 | @type = type |
| 27 | ||
| 28 | loadType: (type) -> | |
| 29 | 78 | try |
| 30 | 78 | CodeMirror.loadMode type |
| 31 | catch e | |
| 32 | 8 | if e.code is 'MODULE_NOT_FOUND' |
| 33 | 8 | logger.debug "#{e}" |
| 34 | 78 | @ |
| 35 | ||
| 36 | tokenize: (code) => | |
| 37 | 78 | @tokens = [] |
| 38 | ||
| 39 | 78 | @loadType @type |
| 40 | ||
| 41 | 78 | CodeMirror.runMode code, @type, (value, tokenType, lineNumber) => |
| 42 | 41690 | return if not lineNumber |
| 43 | 35804 | tokenType = if @isEmptyToken value then 'empty' else tokenType |
| 44 | 35804 | tokenType = tokenType ? 'default' |
| 45 | 35804 | @tokens.push [tokenType, value, lineNumber] |
| 46 | 78 | @ |
| 47 | ||
| 48 | 78 | getType: -> @type |
| 49 | ||
| 50 | generateMap: -> | |
| 51 | 78 | currentMap = "" |
| 52 | 78 | tokensPositions = [] |
| 53 | 78 | for [type, value, lineNumber] in @tokens when @validToken type |
| 54 | 25826 | tokensPositions.push lineNumber |
| 55 | 25826 | currentMap = currentMap + @createMap type, value |
| 56 | ||
| 57 | 78 | tokensPositions: tokensPositions, currentMap: currentMap |
| 58 | ||
| 59 | ||
| 60 | 1 | module.exports = TokenizerCodeMirror |
| 61 |
| Line | Hits | Source |
|---|---|---|
| 1 | ||
| 2 | 1 | TokenizerCodeMirror = require './TokenizerCodeMirror' |
| 3 | ||
| 4 | 1 | class TokenizerFactory |
| 5 | ||
| 6 | tokenizers: {} | |
| 7 | ||
| 8 | LANGUAGES: | |
| 9 | javascript: ['js', 'es', 'es6'] | |
| 10 | typescript: ['ts'] | |
| 11 | jsx: ['jsx'] | |
| 12 | haxe: ['hx', 'hxml'] | |
| 13 | coffeescript: ['coffee'] | |
| 14 | ruby: ['rb'] | |
| 15 | php: ['php', 'phtml'] | |
| 16 | python: ['py'] | |
| 17 | css: ['less', 'css', 'scss'] | |
| 18 | java: ['java'] | |
| 19 | csharp: ['cs'] | |
| 20 | go: ['go'] | |
| 21 | clike: ['cpp', 'c'] | |
| 22 | htmlmixed: ['html', 'htm'] | |
| 23 | yaml: ['yaml', 'yml'] | |
| 24 | ||
| 25 | ||
| 26 | getLanguageByExtension: (extension) -> | |
| 27 | 1181 | for language of TokenizerFactory::LANGUAGES |
| 28 | 9159 | return language if extension in TokenizerFactory::LANGUAGES[language] |
| 29 | 0 | return no |
| 30 | ||
| 31 | getExtensionsByLanguages: (languages) -> | |
| 32 | 51 | languages = [languages] if typeof languages is 'string' |
| 33 | 51 | result = [] |
| 34 | 51 | result.push TokenizerFactory::LANGUAGES[language]... for language of TokenizerFactory::LANGUAGES when language in languages |
| 35 | 51 | return result |
| 36 | ||
| 37 | makeTokenizer: (filename, supportedLanguages) -> | |
| 38 | 1181 | extension = '' |
| 39 | 1181 | matches = filename.match /\.(\w*)$/ |
| 40 | 1181 | extension = matches[1]?.toLowerCase() if matches |
| 41 | ||
| 42 | 1181 | language = TokenizerFactory::getLanguageByExtension extension |
| 43 | ||
| 44 | 1181 | return off if language not in supportedLanguages |
| 45 | ||
| 46 | 88 | if language not of TokenizerFactory::tokenizers |
| 47 | 15 | switch language |
| 48 | when "csharp", "java", "csrc" | |
| 49 | 2 | TokenizerFactory::tokenizers[language] = new TokenizerCodeMirror() |
| 50 | 2 | TokenizerFactory::tokenizers[language].setType "text/x-#{language}" |
| 51 | when "typescript", 'jsx' | |
| 52 | 2 | TokenizerFactory::tokenizers[language] = new TokenizerCodeMirror() |
| 53 | 2 | TokenizerFactory::tokenizers[language].setType "javascript" |
| 54 | else | |
| 55 | 11 | TokenizerFactory::tokenizers[language] = new TokenizerCodeMirror() |
| 56 | 11 | TokenizerFactory::tokenizers[language].setType language |
| 57 | ||
| 58 | 88 | TokenizerFactory::tokenizers[language] |
| 59 | ||
| 60 | 1 | module.exports = TokenizerFactory |
| 61 |