diff --git a/README.md b/README.md index bd51ce5..494092e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,26 @@ Generates a report of churn and complexity for `file1.coffee`, `file2.coffee`, a ## Contributing 1. Fork it -2. Create your feature branch (`git checkout -b my-new-feature`) -3. Commit your changes (`git commit -am 'Add some feature'`) -4. Push to the branch (`git push origin my-new-feature`) -5. Create new Pull Request \ No newline at end of file +1. Create your feature branch (`git checkout -b my-new-feature`) +1. Commit your changes (`git commit -am 'Add some feature'`) +1. Push to the branch (`git push origin my-new-feature`) +1. Create new Pull Request + +## TODO + +Stub out fs read file to speed up CLI specs + +## Known issues + +Method length metric can be incorrect if you have comments at the same level as another function. + +```coffee +fnOne = -> + doSomething() + +# This is what function two does +fnTwo = -> + doSomethingElse() +``` + +In the above example the comment above `fnTwo` will be added to the method length calculation for `fnOne`. diff --git a/bin/clog b/bin/clog index b670418..45e3f47 100755 --- a/bin/clog +++ b/bin/clog @@ -1,38 +1,7 @@ #!/usr/bin/env node -var __slice = [].slice - -var clog = require("../lib/clog").clog +var cli = require("../lib/cli") var argv = require("minimist")(process.argv.slice(2)) -function command() { - var commands = [] - if (arguments.length >= 1) - commands = __slice.call(arguments, 0) - - return commands.reduce(function(memo, c) { - if (argv[c]) { - memo = true - } - return memo - }, false) -} - -var USAGE = "\nUsage: clog path/to/file1.coffee path/to/directory\n\n-h, --help display this message\n-v, --version display the current version" -var message = USAGE - -if (command("h", "help")) { - message = USAGE -} else if (command("v", "version")) { - message = clog.VERSION -} else if (argv._.length) { - if (command("p", "pretty-print")) { - printOptions = { indentSpace: 2 } - } else { - printOptions = {} - } - - message = clog.report(argv._, printOptions) -} - -console.log(message) +console.log(cli(argv)) +process.exit(0) diff --git a/coffeelint.json b/coffeelint.json index eb0755f..6da1a13 100644 --- a/coffeelint.json +++ b/coffeelint.json @@ -16,8 +16,8 @@ } }, "cyclomatic_complexity": { - "value": 10, - "level": "warn" + "value": 7, + "level": "error" }, "duplicate_key": { "level": "error" @@ -120,5 +120,10 @@ }, "transform_messes_up_line_numbers": { "level": "warn" + }, + "no_long_functions": { + "module": "coffeelint-no-long-functions", + "level": "error", + "value": 20 } } diff --git a/lib/cli.js b/lib/cli.js new file mode 100644 index 0000000..1ca53ef --- /dev/null +++ b/lib/cli.js @@ -0,0 +1,40 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var USAGE, clog, isCommand, run; + + clog = require("./clog").clog; + + isCommand = function(arg, commands) { + return commands.reduce(function(memo, c) { + if (arg[c]) { + memo = true; + } + return memo; + }, false); + }; + + USAGE = "Usage: clog [files] [options]\n\nDescription:\n\n Static analysis tool for CoffeeScript code quality.\n\nFiles:\n\n Space separated paths files or directories.\n Directories will be recursed to find\n .coffee, .coffee.md, and .litcoffee files to be analyzed\n\nOptions:\n\n -h, --help display this message\n -v, --version display the current version"; + + run = function(argv) { + var message, printOptions; + message = USAGE; + if (isCommand(argv, ["h", "help"])) { + message = USAGE; + } else if (isCommand(argv, ["v", "version"])) { + message = clog.VERSION; + } else if (argv._.length) { + if (isCommand(argv, ["p", "pretty-print"])) { + printOptions = { + indentSpace: 2 + }; + } else { + printOptions = {}; + } + message = clog.report(argv._, printOptions); + } + return message; + }; + + module.exports = run; + +}).call(this); diff --git a/lib/clog.js b/lib/clog.js index 88a8043..e241b2a 100644 --- a/lib/clog.js +++ b/lib/clog.js @@ -1,94 +1,72 @@ // Generated by CoffeeScript 1.10.0 (function() { - var churn, complexity, count, execSync, files, fs, glob, gpa, isLiterate, report, rules, tokens, tokensForFile; + var analyze, churn, coffee, cyclomaticComplexity, files, fs, functionLength, glob, gpa, letterGrade, nestedCoffeeScriptPattern, report, tokenComplexity, tokens, version; - execSync = require("child_process").execSync; + version = require("../package.json").version; - tokens = require("coffee-script").tokens; + churn = require("./metrics/churn"); - rules = require("../lib/rules"); + tokenComplexity = require("./metrics/token_complexity"); - fs = require("fs"); + cyclomaticComplexity = require("./metrics/cyclomatic_complexity"); - glob = require("glob"); + functionLength = require("./metrics/function_length"); - isLiterate = function(path) { - return /\.litcoffee|\.coffee\.md/i.test(path); - }; + letterGrade = require("./metrics/letter_grade"); - tokensForFile = function(path) { - var file; - file = fs.readFileSync(path, "utf8"); - return tokens(file, { - literate: isLiterate(path) - }); - }; + gpa = require("./metrics/gpa"); - churn = function(filePath) { - var command, output; - command = "git whatchanged " + filePath + " | grep 'commit' | wc -l"; - output = execSync(command); - return parseInt(output, 10); - }; + coffee = require("coffee-script"); - count = function(filePath) { - return tokensForFile(filePath).length; - }; + tokens = coffee.tokens; - complexity = function(filePath) { - return tokensForFile(filePath).reduce(function(sum, token) { - var type; - type = token[0]; - return sum + (rules[type] || 0); - }, 0); - }; + fs = require("fs"); - gpa = function(filePath) { - var base, longFilePenalty, penalized, tokenCount; - tokenCount = count(filePath); - if (tokenCount === 0) { - return 0; - } - if ((0 <= tokenCount && tokenCount <= 200)) { - longFilePenalty = 0; - } else if ((200 < tokenCount && tokenCount <= 300)) { - longFilePenalty = 0.25; - } else if ((300 < tokenCount && tokenCount <= 500)) { - longFilePenalty = 0.5; - } else if (tokenCount > 500) { - longFilePenalty = 1; - } - base = tokenCount / complexity(filePath); - penalized = (base * 4) - longFilePenalty; - return Math.max(penalized, 0); + glob = require("glob"); + + nestedCoffeeScriptPattern = function(path) { + return path + "/**/*\.+(coffee|coffee\.md|litcoffee)"; }; files = function(paths) { return paths.reduce(function(list, path) { - var pattern, stats; + var matchingFiles, stats; stats = fs.lstatSync(path); if (stats.isFile()) { list.push(path); } else if (stats.isDirectory()) { - pattern = path + "/**/*\.+(coffee|coffee\.md|litcoffee)"; - list = list.concat(glob.sync(pattern)); + matchingFiles = glob.sync(nestedCoffeeScriptPattern(path)); + list = list.concat(matchingFiles); } return list; }, []); }; + analyze = function(filePath) { + var file, fileTokens, summary; + file = fs.readFileSync(filePath, "utf8"); + fileTokens = tokens(file, { + literate: coffee.helpers.isLiterate(filePath) + }); + summary = { + churn: churn(filePath), + functionLength: functionLength(file), + cyclomaticComplexity: cyclomaticComplexity(file), + tokenComplexity: tokenComplexity(fileTokens), + tokenCount: fileTokens.length + }; + summary.gpa = gpa(file, summary); + summary.letterGrade = letterGrade(summary.gpa); + return summary; + }; + report = function(filePaths, opts) { var scores; if (opts == null) { opts = {}; } scores = files(filePaths).reduce(function(hash, file) { - hash[file] = { - gpa: gpa(file), - churn: churn(file), - complexity: complexity(file), - tokenCount: count(file) - }; + hash[file] = analyze(file); return hash; }, {}); return JSON.stringify(scores, null, opts.indentSpace); @@ -96,7 +74,8 @@ exports.clog = { report: report, - VERSION: "0.0.12" + analyze: analyze, + VERSION: version }; }).call(this); diff --git a/lib/metrics/churn.js b/lib/metrics/churn.js new file mode 100644 index 0000000..9a0efb1 --- /dev/null +++ b/lib/metrics/churn.js @@ -0,0 +1,16 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var churn, execSync; + + execSync = require("child_process").execSync; + + churn = function(filePath) { + var command, output; + command = "git whatchanged " + filePath + " | grep 'commit' | wc -l"; + output = execSync(command); + return parseInt(output, 10); + }; + + module.exports = churn; + +}).call(this); diff --git a/lib/metrics/cyclomatic_complexity.js b/lib/metrics/cyclomatic_complexity.js new file mode 100644 index 0000000..07f4d53 --- /dev/null +++ b/lib/metrics/cyclomatic_complexity.js @@ -0,0 +1,36 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var CoffeeLint, complexityConfig, cyclomaticComplexity, divide, objectValueMax, objectValueTotal, ref; + + CoffeeLint = require("coffeelint"); + + ref = require("../util"), divide = ref.divide, objectValueMax = ref.objectValueMax, objectValueTotal = ref.objectValueTotal; + + complexityConfig = { + cyclomatic_complexity: { + value: 0, + level: "error" + } + }; + + cyclomaticComplexity = function(file) { + var output; + output = CoffeeLint.lint(file, complexityConfig).reduce(function(hash, description) { + var lineRange; + if (description.rule === "cyclomatic_complexity") { + lineRange = description.lineNumber + "-" + description.lineNumberEnd; + hash.lines[lineRange] = description.context; + } + return hash; + }, { + lines: {} + }); + output.total = objectValueTotal(output.lines); + output.average = divide(output.total, Object.keys(output.lines).length); + output.max = objectValueMax(output.lines); + return output; + }; + + module.exports = cyclomaticComplexity; + +}).call(this); diff --git a/lib/metrics/function_length.js b/lib/metrics/function_length.js new file mode 100644 index 0000000..00e0112 --- /dev/null +++ b/lib/metrics/function_length.js @@ -0,0 +1,38 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var CoffeeLint, divide, functionLength, functionLengthConfig, objectValueMax, objectValueTotal, ref; + + CoffeeLint = require("coffeelint"); + + CoffeeLint.registerRule(require("coffeelint-no-long-functions")); + + ref = require("../util"), divide = ref.divide, objectValueMax = ref.objectValueMax, objectValueTotal = ref.objectValueTotal; + + functionLengthConfig = { + no_long_functions: { + value: 0, + level: "error" + } + }; + + functionLength = function(file) { + var output; + output = CoffeeLint.lint(file, functionLengthConfig).reduce(function(hash, description) { + var lineRange; + if (description.rule === "no_long_functions") { + lineRange = description.lineNumber + "-" + description.lineNumberEnd; + hash.lines[lineRange] = description.lineNumberEnd - description.lineNumber; + } + return hash; + }, { + lines: {} + }); + output.total = objectValueTotal(output.lines); + output.average = divide(output.total, Object.keys(output.lines).length); + output.max = objectValueMax(output.lines); + return output; + }; + + module.exports = functionLength; + +}).call(this); diff --git a/lib/metrics/gpa.js b/lib/metrics/gpa.js new file mode 100644 index 0000000..2a78d3b --- /dev/null +++ b/lib/metrics/gpa.js @@ -0,0 +1,26 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var MAX_GPA, MIN_GPA, clamp, divide, gpa, penalties, ref; + + ref = require("../util"), clamp = ref.clamp, divide = ref.divide; + + penalties = require("../penalties"); + + MIN_GPA = 0; + + MAX_GPA = 4; + + gpa = function(file, metrics) { + var base, complexityPenalty, cyclomaticComplexity, filePenalty, functionLength, functionPenalty, penalized, tokenComplexity, tokenCount; + tokenCount = metrics.tokenCount, tokenComplexity = metrics.tokenComplexity, functionLength = metrics.functionLength, cyclomaticComplexity = metrics.cyclomaticComplexity; + base = divide(tokenCount, tokenComplexity) * MAX_GPA; + filePenalty = penalties.longFile(tokenCount); + functionPenalty = penalties.longFunction(functionLength.average); + complexityPenalty = penalties.complexFile(cyclomaticComplexity.total); + penalized = base * filePenalty * functionPenalty * complexityPenalty; + return clamp(penalized, MIN_GPA, MAX_GPA); + }; + + module.exports = gpa; + +}).call(this); diff --git a/lib/metrics/letter_grade.js b/lib/metrics/letter_grade.js new file mode 100644 index 0000000..f24f858 --- /dev/null +++ b/lib/metrics/letter_grade.js @@ -0,0 +1,21 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var letterGrade; + + letterGrade = function(numericGrade) { + if ((0 <= numericGrade && numericGrade <= 0.8)) { + return "F"; + } else if ((0.8 < numericGrade && numericGrade <= 1.6)) { + return "D"; + } else if ((1.6 < numericGrade && numericGrade <= 2.4)) { + return "C"; + } else if ((2.4 < numericGrade && numericGrade <= 3.2)) { + return "B"; + } else if ((3.2 < numericGrade && numericGrade <= 4)) { + return "A"; + } + }; + + module.exports = letterGrade; + +}).call(this); diff --git a/lib/metrics/token_complexity.js b/lib/metrics/token_complexity.js new file mode 100644 index 0000000..9069707 --- /dev/null +++ b/lib/metrics/token_complexity.js @@ -0,0 +1,17 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var rules, tokenComplexity; + + rules = require("../rules"); + + tokenComplexity = function(tokens) { + return tokens.reduce(function(sum, token) { + var type; + type = token[0]; + return sum += rules[type] || 0; + }, 0); + }; + + module.exports = tokenComplexity; + +}).call(this); diff --git a/lib/penalties.js b/lib/penalties.js new file mode 100644 index 0000000..37eac46 --- /dev/null +++ b/lib/penalties.js @@ -0,0 +1,47 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var complexFile, longFile, longFunction; + + complexFile = function(complexity) { + if ((0 <= complexity && complexity <= 20)) { + return 1; + } else if ((20 < complexity && complexity <= 30)) { + return 0.9; + } else if ((30 < complexity && complexity <= 40)) { + return 0.8; + } else if (complexity > 40) { + return 0.7; + } + }; + + longFunction = function(averageFunctionLength) { + if ((0 <= averageFunctionLength && averageFunctionLength <= 20)) { + return 1; + } else if ((20 < averageFunctionLength && averageFunctionLength <= 40)) { + return 0.9; + } else if ((40 < averageFunctionLength && averageFunctionLength <= 60)) { + return 0.8; + } else if (averageFunctionLength > 60) { + return 0.7; + } + }; + + longFile = function(tokens) { + if ((0 <= tokens && tokens <= 1000)) { + return 1; + } else if ((1000 < tokens && tokens <= 2000)) { + return 0.9; + } else if ((2000 < tokens && tokens <= 4000)) { + return 0.8; + } else if (tokens > 4000) { + return 0.7; + } + }; + + module.exports = { + complexFile: complexFile, + longFile: longFile, + longFunction: longFunction + }; + +}).call(this); diff --git a/lib/rules.js b/lib/rules.js index 659af39..506ea65 100644 --- a/lib/rules.js +++ b/lib/rules.js @@ -7,47 +7,46 @@ ",": 1, "-": 1, ":": 1, - ".": 2, - "[": 2, - "{": 2, - "?": 3, - "->": 3, - "++": 3, - "--": 4, - "@": 5, - "?.": 5, - "=>": 6, "BOOL": 1, - "CALL_END": 0, - "CALL_START": 2, - "CLASS": 30, "COMPARE": 1, - "COMPOUND_ASSIGN": 2, - "ELSE": 2, - "EXTENDS": 15, - "FOR": 10, - "FORIN": 10, - "FOROF": 10, "IDENTIFIER": 1, - "IF": 4, "INDENT": 1, - "INDEX_START": 2, "LEADING_WHEN": 1, "LOGIC": 1, "MATH": 1, - "NULL": 3, "NUMBER": 1, - "PARAM_START": 3, - "REGEX": 10, - "RELATION": 3, - "RETURN": 0, - "SHIFT": 4, "STRING": 1, - "SUPER": 7, - "SWITCH": 7, "TERMINATOR": 1, - "UNARY": 3, - "UNARY_MATH": 1 + "UNARY_MATH": 1, + "CALL_START": 1, + "CALL_END": 1, + ".": 2, + "[": 2, + "{": 2, + "COMPOUND_ASSIGN": 2, + "INDEX_START": 2, + "PARAM_START": 2, + "->": 2.5, + "?": 2.5, + "RELATION": 2.5, + "SHIFT": 2.5, + "UNARY": 2.5, + "++": 2.75, + "--": 3, + "=>": 3, + "ELSE": 3, + "IF": 3, + "NULL": 3, + "REGEX": 3, + "SWITCH": 3, + "@": 3.25, + "?.": 3.25, + "FOR": 3.5, + "FORIN": 3.5, + "FOROF": 3.5, + "SUPER": 3.5, + "EXTENDS": 3.75, + "CLASS": 4 }; }).call(this); diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..29a1877 --- /dev/null +++ b/lib/util.js @@ -0,0 +1,42 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var clamp, divide, objectValueMax, objectValueTotal; + + clamp = function(number, min, max) { + return Math.max(Math.min(max, number), min); + }; + + divide = function(numerator, denominator) { + if (denominator) { + return numerator / denominator; + } else { + return 0; + } + }; + + objectValueTotal = function(obj) { + return Object.keys(obj).reduce(function(memo, key) { + memo += obj[key]; + return memo; + }, 0); + }; + + objectValueMax = function(obj) { + return Object.keys(obj).reduce(function(memo, key) { + var val; + val = obj[key]; + if (val > memo) { + memo = val; + } + return memo; + }, 0); + }; + + module.exports = { + clamp: clamp, + divide: divide, + objectValueMax: objectValueMax, + objectValueTotal: objectValueTotal + }; + +}).call(this); diff --git a/package.json b/package.json index 25ea882..f3f2bfb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "author": "Matt Diebolt", "name": "clog-analysis", "description": "Simple CoffeeScript static analysis for code quality metrics", - "version": "0.0.12", + "version": "1.0.0", + "main": "lib/clog.js", "repository": { "type": "git", "url": "https://github.com/mdiebolt/clog.git" @@ -11,17 +12,20 @@ "clog": "./bin/clog" }, "scripts": { - "lint": "coffeelint -q source test", - "test": "NODE_ENV=test mocha --compilers coffee:coffee-script/register --colors && npm run lint", - "watch": "coffee -wco lib source" + "compile": "coffee -co lib source", + "lint": "coffeelint -q source", + "pretest": "npm run compile", + "test": "mocha && npm run lint", + "prepublish": "npm test" }, "dependencies": { "coffee-script": "^1.10.0", + "coffeelint": "^1.13.0", + "coffeelint-no-long-functions": "git://github.com/mdiebolt/coffeelint-no-long-functions.git", "glob": "^5.0.15", "minimist": "^1.2.0" }, "devDependencies": { - "coffeelint": "^1.13.0", "coffeelint-prefer-double-quotes": "^0.1.0", "coffeelint-prefer-symbol-operator": "^0.1.1", "mocha": "^2.3.3" diff --git a/source/cli.coffee b/source/cli.coffee new file mode 100644 index 0000000..eade41d --- /dev/null +++ b/source/cli.coffee @@ -0,0 +1,45 @@ +{clog} = require("./clog") + +isCommand = (arg, commands) -> + commands.reduce (memo, c) -> + memo = true if arg[c] + memo + , false + +USAGE = """ + Usage: clog [files] [options] + + Description: + + Static analysis tool for CoffeeScript code quality. + + Files: + + Space separated paths files or directories. + Directories will be recursed to find + .coffee, .coffee.md, and .litcoffee files to be analyzed + + Options: + + -h, --help display this message + -v, --version display the current version +""" + +run = (argv) -> + message = USAGE + + if isCommand(argv, ["h", "help"]) + message = USAGE + else if isCommand(argv, ["v", "version"]) + message = clog.VERSION + else if argv._.length + if isCommand(argv, ["p", "pretty-print"]) + printOptions = { indentSpace: 2 } + else + printOptions = {} + + message = clog.report(argv._, printOptions) + + message + +module.exports = run diff --git a/source/clog.coffee b/source/clog.coffee index 10a38d3..d7e1a79 100644 --- a/source/clog.coffee +++ b/source/clog.coffee @@ -1,88 +1,24 @@ -# Clog +{version} = require "../package.json" -# A simple static analysis tool for CoffeeScript source code. -# Leverages CoffeeScript compiler, walking over all tokens -# in a file and weighing the code based on a number of heuristics -# corresponding to the token type. +# Metrics +churn = require "./metrics/churn" +tokenComplexity = require "./metrics/token_complexity" +cyclomaticComplexity = require "./metrics/cyclomatic_complexity" +functionLength = require "./metrics/function_length" +letterGrade = require "./metrics/letter_grade" +gpa = require "./metrics/gpa" -{execSync} = require "child_process" -{tokens} = require "coffee-script" -rules = require "../lib/rules" +coffee = require "coffee-script" +{tokens} = coffee fs = require "fs" glob = require "glob" -# Helper to determine if filePath represents a literate Coffee file. - -isLiterate = (path) -> - /\.litcoffee|\.coffee\.md/i.test(path) - -# Helper to return tokens for a given file path. - -tokensForFile = (path) -> - file = fs.readFileSync(path, "utf8") - - tokens file, - literate: isLiterate(path) - -## Metric: Churn - -# Indicates how many times a file has been changed. -# The more it has been changed, the better a candidate it is for refactoring. - -# Grep for commit since `git whatchanged` shows -# multiple lines of details from each commit. -churn = (filePath) -> - command = "git whatchanged #{filePath} | grep 'commit' | wc -l" - output = execSync command - parseInt(output, 10) - -## Metric: Token count - -# The number of tokens in the file. -# Used in conjunction with token score to determine gpa. - -count = (filePath) -> - tokensForFile(filePath).length - -## Metric: Token complexity - -# Determines how complex code is by weighing each token based on maintainability. -# Using tokens is style agnostic and won't change based on -# comment / documentation style, or from personal whitespace style. - -complexity = (filePath) -> - tokensForFile(filePath).reduce (sum, token) -> - type = token[0] - sum + (rules[type] || 0) - , 0 - -## Metric: Complexity per token - -# Gives the file a grade based on it's token complexity compared to token length. -# Scaled from 0-4. - -gpa = (filePath) -> - tokenCount = count(filePath) - return 0 if tokenCount == 0 - - if 0 <= tokenCount <= 200 - longFilePenalty = 0 - else if 200 < tokenCount <= 300 - longFilePenalty = 0.25 - else if 300 < tokenCount <= 500 - longFilePenalty = 0.5 - else if tokenCount > 500 - longFilePenalty = 1 - - base = tokenCount / complexity(filePath) - penalized = (base * 4) - longFilePenalty - - Math.max(penalized, 0) - -# Return an array of CoffeeScript files based on file filePaths or directories -# passed in. +nestedCoffeeScriptPattern = (path) -> + path + "/**/*\.+(coffee|coffee\.md|litcoffee)" +# Return an array of CoffeeScript files +# based on file filePaths or directories passed in files = (paths) -> paths.reduce (list, path) -> stats = fs.lstatSync(path) @@ -90,29 +26,48 @@ files = (paths) -> if stats.isFile() list.push path else if stats.isDirectory() - pattern = "#{path}/**/*\.+(coffee|coffee\.md|litcoffee)" - list = list.concat(glob.sync pattern) + matchingFiles = glob.sync nestedCoffeeScriptPattern(path) + list = list.concat(matchingFiles) list , [] -# Output scores per file. +# Metric summary for an individual file +analyze = (filePath) -> + file = fs.readFileSync(filePath, "utf8") + + fileTokens = tokens file, + literate: coffee.helpers.isLiterate(filePath) + summary = + churn: churn(filePath) + functionLength: functionLength(file) + cyclomaticComplexity: cyclomaticComplexity(file) + tokenComplexity: tokenComplexity(fileTokens) + tokenCount: fileTokens.length + + # calculate GPA based on other metrics + summary.gpa = gpa(file, summary) + summary.letterGrade = letterGrade(summary.gpa) + + summary + +# Output scores per file report = (filePaths, opts = {}) -> scores = files(filePaths).reduce (hash, file) -> - hash[file] = - gpa: gpa(file) - churn: churn(file) - complexity: complexity(file) - tokenCount: count(file) - + hash[file] = analyze(file) hash , {} JSON.stringify(scores, null, opts.indentSpace) -# Export public API. +# Clog +# A simple static analysis tool for CoffeeScript source code. +# Leverages CoffeeScript compiler, walking over all tokens +# in a file and weighing the code based on a heuristic +# corresponding to the token type. exports.clog = report: report - VERSION: "0.0.12" + analyze: analyze + VERSION: version diff --git a/source/metrics/churn.coffee b/source/metrics/churn.coffee new file mode 100644 index 0000000..4c2f06a --- /dev/null +++ b/source/metrics/churn.coffee @@ -0,0 +1,15 @@ +{execSync} = require "child_process" + +## Metric: Churn + +# Indicates how many times a file has been changed. +# The more it has been changed, the better a candidate it is for refactoring. + +# Grep for commit since `git whatchanged` shows +# multiple lines of details from each commit. +churn = (filePath) -> + command = "git whatchanged #{filePath} | grep 'commit' | wc -l" + output = execSync command + parseInt(output, 10) + +module.exports = churn diff --git a/source/metrics/cyclomatic_complexity.coffee b/source/metrics/cyclomatic_complexity.coffee new file mode 100644 index 0000000..378cd21 --- /dev/null +++ b/source/metrics/cyclomatic_complexity.coffee @@ -0,0 +1,29 @@ +CoffeeLint = require "coffeelint" +{divide, objectValueMax, objectValueTotal} = require "../util" + +complexityConfig = + cyclomatic_complexity: + value: 0 + level: "error" + +# Metric: Cyclomatic complexity + +# A number representing how complex a file is +# Piggybacking off the implementation from CoffeeLint +# Report each function's complexity by setting CoffeeLint error threshold to 0 +cyclomaticComplexity = (file) -> + output = CoffeeLint.lint(file, complexityConfig).reduce (hash, description) -> + if description.rule == "cyclomatic_complexity" + lineRange = description.lineNumber + "-" + description.lineNumberEnd + hash.lines[lineRange] = description.context + + hash + , {lines: {}} + + output.total = objectValueTotal(output.lines) + output.average = divide(output.total, Object.keys(output.lines).length) + output.max = objectValueMax(output.lines) + + output + +module.exports = cyclomaticComplexity diff --git a/source/metrics/function_length.coffee b/source/metrics/function_length.coffee new file mode 100644 index 0000000..e8698f8 --- /dev/null +++ b/source/metrics/function_length.coffee @@ -0,0 +1,34 @@ +CoffeeLint = require "coffeelint" +CoffeeLint.registerRule require "coffeelint-no-long-functions" + +{divide, objectValueMax, objectValueTotal} = require "../util" + +functionLengthConfig = + no_long_functions: + value: 0 + level: "error" + +## Metric: Function length + +# Return the number of lines per function. +# Piggybacking off CoffeeLint implementation +# Report each function's length by setting CoffeeLint error threshold to 0 +# TODO: fix this. Right now CoffeeLint's function length plugin incorrectly +# counts comments preceeding a method as part of the same indentation level +# method above it. +functionLength = (file) -> + output = CoffeeLint.lint(file, functionLengthConfig).reduce (hash, description) -> + if description.rule == "no_long_functions" + lineRange = description.lineNumber + "-" + description.lineNumberEnd + hash.lines[lineRange] = description.lineNumberEnd - description.lineNumber + + hash + , {lines: {}} + + output.total = objectValueTotal(output.lines) + output.average = divide(output.total, Object.keys(output.lines).length) + output.max = objectValueMax(output.lines) + + output + +module.exports = functionLength diff --git a/source/metrics/gpa.coffee b/source/metrics/gpa.coffee new file mode 100644 index 0000000..2cf261a --- /dev/null +++ b/source/metrics/gpa.coffee @@ -0,0 +1,23 @@ +{clamp, divide} = require "../util" +penalties = require "../penalties" + +MIN_GPA = 0 +MAX_GPA = 4 + +## Metric: GPA + +# Gives the file a grade between 0-4 +# based on token complexity compared to token length +gpa = (file, metrics) -> + {tokenCount, tokenComplexity, functionLength, cyclomaticComplexity} = metrics + base = divide(tokenCount, tokenComplexity) * MAX_GPA + + filePenalty = penalties.longFile(tokenCount) + functionPenalty = penalties.longFunction(functionLength.average) + complexityPenalty = penalties.complexFile(cyclomaticComplexity.total) + + penalized = base * filePenalty * functionPenalty * complexityPenalty + + clamp(penalized, MIN_GPA, MAX_GPA) + +module.exports = gpa diff --git a/source/metrics/letter_grade.coffee b/source/metrics/letter_grade.coffee new file mode 100644 index 0000000..e244eca --- /dev/null +++ b/source/metrics/letter_grade.coffee @@ -0,0 +1,16 @@ +# Metric: Letter grade + +# Translate score into letter grade on A-F scale +letterGrade = (numericGrade) -> + if 0 <= numericGrade <= 0.8 + "F" + else if 0.8 < numericGrade <= 1.6 + "D" + else if 1.6 < numericGrade <= 2.4 + "C" + else if 2.4 < numericGrade <= 3.2 + "B" + else if 3.2 < numericGrade <= 4 + "A" + +module.exports = letterGrade diff --git a/source/metrics/token_complexity.coffee b/source/metrics/token_complexity.coffee new file mode 100644 index 0000000..4981f8f --- /dev/null +++ b/source/metrics/token_complexity.coffee @@ -0,0 +1,14 @@ +rules = require "../rules" + +# Metric: Token complexity + +# Determines how complex code is by weighing each token based on maintainability. +# Using tokens is style agnostic and won't change based on +# comment / documentation style, or from personal whitespace style. +tokenComplexity = (tokens) -> + tokens.reduce (sum, token) -> + type = token[0] + sum += (rules[type] || 0) + , 0 + +module.exports = tokenComplexity diff --git a/source/penalties.coffee b/source/penalties.coffee new file mode 100644 index 0000000..f9aa8f6 --- /dev/null +++ b/source/penalties.coffee @@ -0,0 +1,35 @@ +complexFile = (complexity) -> + if 0 <= complexity <= 20 + 1 + else if 20 < complexity <= 30 + 0.9 + else if 30 < complexity <= 40 + 0.8 + else if complexity > 40 + 0.7 + +longFunction = (averageFunctionLength) -> + if 0 <= averageFunctionLength <= 20 + 1 + else if 20 < averageFunctionLength <= 40 + 0.9 + else if 40 < averageFunctionLength <= 60 + 0.8 + else if averageFunctionLength > 60 + 0.7 + +longFile = (tokens) -> + if 0 <= tokens <= 1000 + 1 + else if 1000 < tokens <= 2000 + 0.9 + else if 2000 < tokens <= 4000 + 0.8 + else if tokens > 4000 + 0.7 + +module.exports = { + complexFile + longFile + longFunction +} diff --git a/source/rules.coffee b/source/rules.coffee index 764a34b..0f604a0 100644 --- a/source/rules.coffee +++ b/source/rules.coffee @@ -1,5 +1,4 @@ # Rules for how each token type is weighted in terms of maintainability. - module.exports = "+": 1 "=": 1 @@ -8,49 +7,54 @@ module.exports = "-": 1 ":": 1 - ".": 2 - "[": 2 - "{": 2 - - "?": 3 - "->": 3 - "++": 3 - - "--": 4 - - "@": 5 - "?.": 5 - - "=>": 6 - "BOOL": 1 - "CALL_END": 0 - "CALL_START": 2 - "CLASS": 30 "COMPARE": 1 - "COMPOUND_ASSIGN": 2 - "ELSE": 2 - "EXTENDS": 15 - "FOR": 10 - "FORIN": 10 - "FOROF": 10 "IDENTIFIER": 1 - "IF": 4 "INDENT": 1 - "INDEX_START": 2 "LEADING_WHEN": 1 "LOGIC": 1 "MATH": 1 - "NULL": 3 "NUMBER": 1 - "PARAM_START": 3 - "REGEX": 10 - "RELATION": 3 - "RETURN": 0 - "SHIFT": 4 "STRING": 1 - "SUPER": 7 - "SWITCH": 7 "TERMINATOR": 1 - "UNARY": 3 "UNARY_MATH": 1 + "CALL_START": 1 + "CALL_END": 1 + + ".": 2 + "[": 2 + "{": 2 + + "COMPOUND_ASSIGN": 2 + "INDEX_START": 2 + "PARAM_START": 2 + + "->": 2.5 + "?": 2.5 + + "RELATION": 2.5 + "SHIFT": 2.5 + "UNARY": 2.5 + + "++": 2.75 + + "--": 3 + "=>": 3 + + "ELSE": 3 + "IF": 3 + "NULL": 3 + "REGEX": 3 + "SWITCH": 3 + + "@": 3.25 + "?.": 3.25 + + "FOR": 3.5 + "FORIN": 3.5 + "FOROF": 3.5 + "SUPER": 3.5 + + "EXTENDS": 3.75 + + "CLASS": 4 diff --git a/source/util.coffee b/source/util.coffee new file mode 100644 index 0000000..ca7b4ff --- /dev/null +++ b/source/util.coffee @@ -0,0 +1,32 @@ +# Force number to be within min and max +clamp = (number, min, max) -> + Math.max(Math.min(max, number), min) + +# Divide, avoiding division by 0 error +divide = (numerator, denominator) -> + if denominator + numerator / denominator + else + 0 + +# Sum the values of an object literal +objectValueTotal = (obj) -> + Object.keys(obj).reduce (memo, key) -> + memo += obj[key] + memo + , 0 + +# Find the max value of an object literal +objectValueMax = (obj) -> + Object.keys(obj).reduce (memo, key) -> + val = obj[key] + memo = val if val > memo + memo + , 0 + +module.exports = { + clamp + divide + objectValueMax + objectValueTotal +} diff --git a/test/cli.coffee b/test/cli.coffee index d97312d..aec10ec 100644 --- a/test/cli.coffee +++ b/test/cli.coffee @@ -1,40 +1,74 @@ assert = require "assert" -{execSync} = require "child_process" +cli = require "../lib/cli" -describe "cli", -> - it "prints out usage instructions", -> - output = execSync "./bin/clog" +SEMVER_PATTERN = /\d+\.\d+\.\d+/ - assert.ok(output.toString("utf-8").indexOf("Usage:") >= 0) +describe "CLI", -> + describe "instructions", -> + it "outputs when no arguments are passed", -> + output = cli + _: [] - describe "reports", -> - it "supports passing in a directory", -> - output = execSync "./bin/clog test" + assert.ok(/Usage/.test(output)) + + describe "help", -> + it "outputs instructions with short flag", -> + output = cli + _: [] + h: true + + assert.ok(/Usage/.test(output)) + + it "outputs instructions with short flag", -> + output = cli + _: [] + help: true + + assert.ok(/Usage/.test(output)) - assert.ok(output.length > 0) + describe "version", -> + it "outputs version with short flag", -> + output = cli + _: [] + v: true - it "supports passing in the current directory", -> - output = JSON.parse(execSync "./bin/clog .") + assert.ok(SEMVER_PATTERN.test(output)) - assert.ok(output["./source/clog.coffee"].churn?) - assert.ok(output["./test/clog.coffee"].tokenCount?) + it "outputs version with long flag", -> + output = cli + _: [] + version: true + assert.ok(SEMVER_PATTERN.test(output)) + + describe "reporting on directories", -> + it "supports passing in a directory", -> + output = cli + _: ["test"] + + assert.ok(output.length) + + describe "reporting on files", -> it "supports passing in a single file", -> - output = JSON.parse(execSync "./bin/clog test/fixtures/case.coffee") + report = cli + _: ["test/fixtures/case.coffee"] - assert.ok(output["test/fixtures/case.coffee"].complexity?) + output = JSON.parse(report) + assert.ok(output["test/fixtures/case.coffee"].gpa?) it "supports passing in multiple files", -> - command = "./bin/clog test/fixtures/nested_ifs.coffee source/rules.coffee" - output = JSON.parse(execSync command) + report = cli + _: ["test/fixtures/nested_ifs.coffee", "source/rules.coffee"] + output = JSON.parse(report) assert.ok(output["test/fixtures/nested_ifs.coffee"].gpa?) - assert.ok(output["source/rules.coffee"].churn?) + assert.ok(output["source/rules.coffee"].gpa?) it "supports passing in a mix of directories and files", -> - command = "./bin/clog source test/cli.coffee" - output = JSON.parse(execSync command) + report = cli + _: ["source", "test/cli.coffee"] + output = JSON.parse(report) assert.ok(output["source/rules.coffee"].gpa?) assert.ok(output["source/clog.coffee"].gpa?) - assert.ok(output["test/cli.coffee"].churn?) + assert.ok(output["test/cli.coffee"].gpa?) diff --git a/test/clog.coffee b/test/clog.coffee index 8df44e6..037ff67 100644 --- a/test/clog.coffee +++ b/test/clog.coffee @@ -5,27 +5,83 @@ assert = require "assert" fixturePath = (name) -> "#{__dirname}/fixtures/#{name}.coffee" -describe "Clog", -> - describe "churn", -> +describe "churn", -> + scores = null + + beforeEach -> scores = JSON.parse clog.report [__filename] - it "counts the number of changes to the file", -> - assert.ok(scores[__filename].churn > 0) + it "counts the number of changes to the file", -> + assert.ok(scores[__filename].churn > 0) + +describe "classes", -> + file = scores = null + + beforeEach -> + file = fixturePath("class") + scores = JSON.parse clog.report [file] + + it "return correct report", -> + assert.ok(scores[file].churn?) + assert.equal(6, scores[file].tokenComplexity) + assert.equal(2, scores[file].gpa.toFixed(2)) + assert.equal(3, scores[file].tokenCount) + assert.equal("C", scores[file].letterGrade) + +describe "nested if statements", -> + file = scores = null + + beforeEach -> + file = fixturePath("nested_ifs") + scores = JSON.parse clog.report [file] + + it "return correct report", -> + assert.ok(scores[file].churn?) + assert.equal(21, scores[file].tokenComplexity) + assert.equal(3.43, scores[file].gpa.toFixed(2)) + assert.equal(18, scores[file].tokenCount) + assert.equal("A", scores[file].letterGrade) + +describe "case statements", -> + file = scores = null + + beforeEach -> + file = fixturePath("case") + scores = JSON.parse clog.report [file] + + it "return correct report", -> + assert.ok(scores[file].churn?) + assert.equal(scores[file].tokenComplexity, 26) + assert.equal(scores[file].gpa.toFixed(2), 4) + assert.equal(scores[file].tokenCount, 26) + assert.equal(scores[file].letterGrade, "A") + +describe "long files", -> + file = scores = null + + beforeEach -> + file = fixturePath("long_file") + scores = JSON.parse clog.report [file] + + it "correctly penalizes", -> + assert.equal(scores[file].gpa.toFixed(2), 2.8, "GPA") + +describe "complex files", -> + file = scores = null - describe "#report", -> - cases = fixturePath("case") - ifs = fixturePath("nested_ifs") + beforeEach -> + file = fixturePath("complex") + scores = JSON.parse clog.report [file] - scores = JSON.parse clog.report [cases, ifs] + it "correctly penalizes", -> + assert.equal(scores[file].gpa.toFixed(2), 2.3, "GPA") - it "returns token compexity", -> - assert.equal(scores[cases].complexity, 29) - assert.equal(scores[ifs].complexity, 22) +describe "long function length", -> + file = scores = null - it "returns gpa", -> - assert.equal(scores[cases].gpa.toFixed(2), 3.59) - assert.equal(scores[ifs].gpa.toFixed(2), 3.27) + beforeEach -> + file = fixturePath("long_function") + scores = JSON.parse clog.report [file] - it "returns token count", -> - assert.equal(scores[cases].tokenCount, 26) - assert.equal(scores[ifs].tokenCount, 18) + it "correctly penalizes", -> + assert.equal(scores[file].gpa.toFixed(2), 2.79, "GPA") diff --git a/test/fixtures/class.coffee b/test/fixtures/class.coffee new file mode 100644 index 0000000..360d84d --- /dev/null +++ b/test/fixtures/class.coffee @@ -0,0 +1 @@ +class Something diff --git a/test/fixtures/complex.coffee b/test/fixtures/complex.coffee new file mode 100644 index 0000000..eef560a --- /dev/null +++ b/test/fixtures/complex.coffee @@ -0,0 +1,59 @@ +fn = -> + if outerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + doSomething() + +secondFn = -> + if outerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + doSomething() + +thirdFn = -> + if outerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + doSomething() + +fourthFn = -> + if outerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + if innerCondition + doSomething() diff --git a/test/fixtures/long_file.coffee b/test/fixtures/long_file.coffee new file mode 100644 index 0000000..412430c --- /dev/null +++ b/test/fixtures/long_file.coffee @@ -0,0 +1,1001 @@ +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() +doStuff() diff --git a/test/fixtures/long_function.coffee b/test/fixtures/long_function.coffee new file mode 100644 index 0000000..8711df3 --- /dev/null +++ b/test/fixtures/long_function.coffee @@ -0,0 +1,62 @@ +fn = -> + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() + run() diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..50cb089 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,2 @@ +--compilers coffee:coffee-script/register +--colors