diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..4c8045e --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,53 @@ +version: 2.1 + +jobs: + build: + machine: + docker_layer_caching: true + working_directory: ~/codeclimate/codeclimate-fixme + steps: + - checkout + - run: + name: Build + command: make image + - run: + name: Test + command: make test + + release_images: + machine: + docker_layer_caching: true + working_directory: ~/codeclimate/codeclimate-fixme + steps: + - checkout + - run: + name: Validate owner + command: | + if [ "$CIRCLE_PROJECT_USERNAME" -ne "codeclimate" ] + then + echo "Skipping release for non-codeclimate branches" + circleci step halt + fi + - run: make image + - run: echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin + - run: + name: Push image to Dockerhub + command: | + make release RELEASE_TAG="b$CIRCLE_BUILD_NUM" + make release RELEASE_TAG="$(echo $CIRCLE_BRANCH | grep -oP 'channel/\K[\w\-]+')" +workflows: + version: 2 + build_deploy: + jobs: + - build + - release_images: + context: Quality + requires: + - build + filters: + branches: + only: /master|channel\/[\w-]+/ + +notify: + webhooks: + - url: https://cc-slack-proxy.herokuapp.com/circle diff --git a/.codeclimate.yml b/.codeclimate.yml index 6a739d8..69f051b 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,8 +1,11 @@ engines: fixme: + enabled: false + eslint: enabled: true +ratings: + paths: + - "**.js" exclude_paths: -- "**/*.md" -- "Dockerfile" -- "bin/fixme" -- "index.js" +- "node_modules/" +- "test/fixtures/" diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..85dcc16 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.git +node_modules diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..96212a3 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/*{.,-}min.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..d59da74 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + "env": { + "node": true, // When in a backend context + "es6": true, + }, + "rules": { + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "comma-style": [2, "first", { exceptions: {ArrayExpression: true, ObjectExpression: true} }], + "complexity": [2, 6], + "curly": 2, + "eqeqeq": [2, "allow-null"], + "no-shadow-restricted-names": 2, + "no-undef": 2, + "no-use-before-define": 2, + "radix": 2, + "semi": 2, + "space-infix-ops": 2, + "strict": 0, + }, + /** + * globals should be defined per file when possible. Use the directive here + * when there are project-level globals (such as jquery) + */ + "globals": {}, +}; diff --git a/Dockerfile b/Dockerfile index 1aecb17..91ab0c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,20 @@ -FROM node +FROM node:6-alpine +LABEL maintainer="Code Climate " -MAINTAINER Michael R. Bernstein +WORKDIR /usr/src/app/ -RUN useradd -u 9000 -r -s /bin/false app - -RUN npm install glob - -WORKDIR /code -COPY . /usr/src/app COPY engine.json / +COPY package.json ./ + +# Install dependencies: +RUN apk add --no-cache --virtual .run-deps grep && npm install +RUN adduser -u 9000 -S -s /bin/false app USER app + +COPY . ./ + VOLUME /code +WORKDIR /code CMD ["/usr/src/app/bin/fixme"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0be2e65 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.PHONY: image test release + +IMAGE_NAME ?= codeclimate/codeclimate-fixme +RELEASE_REGISTRY ?= codeclimate + +ifndef RELEASE_TAG +override RELEASE_TAG = latest +endif + +image: + docker build --rm -t $(IMAGE_NAME) . + +test: image + docker run --rm -v $$PWD/test/fixtures:/code $(IMAGE_NAME) sh -c "cd /usr/src/app && npm test" + +release: + docker tag $(IMAGE_NAME) $(RELEASE_REGISTRY)/codeclimate-fixme:$(RELEASE_TAG) + docker push $(RELEASE_REGISTRY)/codeclimate-fixme:$(RELEASE_TAG) diff --git a/README.md b/README.md index 7516d87..a30c216 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,31 @@ -# Code Climate FIXME Engine +# Try Qlty today, the newest edition of Code Climate Quality. +#### This repository is deprecated and archived. -`codeclimate-fixme` is a Code Climate engine that finds comments in your code which match the following strings: +This is a repository for a Code Climate Quality plugin which is packaged as a Docker image. -* `TODO` -* `FIXME` -* `HACK` -* `BUG` +Code Climate Quality is being replaced with the new [Qlty](qlty.sh) code quality platform. Qlty uses a new plugin system which does not require packaging plugins as Docker images. -These strings are things you should fix now, not later. +As a result, this repository is no longer maintained and has been archived. -`codeclimate-fixme` is also very simple, and is intended to provide a `Hello World` like template for Code Climate Platform engine authors. It is implemented in JavaScript as an NPM package. +## Advantages of Qlty plugins +The new Qlty plugins system provides key advantages over the older, Docker-based plugin system: -### Installation & Usage +- Linting runs much faster without the overhead of virtualization +- New versions of linters are available immediately without needing to wait for a re-packaged release +- Plugins can be run with any arbitrary extensions (like extra rules and configs) without requiring pre-packaging +- Eliminates security issues associated with exposing a Docker daemon -1. If you haven't already, [install the Code Climate CLI](https://github.com/codeclimate/codeclimate). -2. Run `codeclimate engines:enable fixme`. This command both installs the engine and enables it in your `.codeclimate.yml` file. -3. You're ready to analyze! Browse into your project's folder and run `codeclimate analyze`. +## Try out Qlty today free -### Need help? +[Qlty CLI](https://docs.qlty.sh/cli/quickstart) is the fastest linter and auto-formatter for polyglot teams. It is completely free and available for Mac, Windows, and Linux. -For help with `codeclimate-fixme`, please open an issue on this repository. + - Install Qlty CLI: +` +curl https://qlty.sh | sh # Mac or Linux +` +or ` powershell -c "iwr https://qlty.sh | iex" # Windows` -If you're running into a Code Climate issue, first look over this project's [GitHub Issues](https://github.com/codeclimate/codeclimate-watson/issues), as your question may have already been covered. If not, [go ahead and open a support ticket with us](https://codeclimate.com/help). +[Qlty Cloud](https://docs.qlty.sh/cloud/quickstart) is a full code health platform for integrating code quality into development team workflows. It is free for unlimited private contributors. + - [Try Qlty Cloud today](https://docs.qlty.sh/cloud/quickstart) + +**Note**: For existing customers of Quality, please see our [Migration Guide](https://docs.qlty.sh/migration/guide) for more information and resources. diff --git a/bin/fixme b/bin/fixme index e5c41e2..f60c1c9 100755 --- a/bin/fixme +++ b/bin/fixme @@ -1,6 +1,13 @@ #!/usr/bin/env node -var FixMe = require('../index'); -var fixMe = new FixMe(); +var fs = require('fs'); +var FixMe = require('../lib/fix-me'); +var config; -fixMe.runEngine(); +fs.readFile('/config.json', function(err, data) { + if (!err) { + config = JSON.parse(data); + } + + new FixMe().run(config) +}); diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 012fc78..0000000 --- a/circle.yml +++ /dev/null @@ -1,25 +0,0 @@ -machine: - services: - - docker - environment: - CLOUDSDK_CORE_DISABLE_PROMPTS: 1 - PRIVATE_REGISTRY: us.gcr.io/code_climate - -test: - override: - - docker build -t=$PRIVATE_REGISTRY/$CIRCLE_PROJECT_REPONAME:b$CIRCLE_BUILD_NUM . - -deployment: - registry: - branch: master - owner: codeclimate - commands: - - echo $GCLOUD_JSON_KEY_BASE64 | sed 's/ //g' | base64 -d > /tmp/gcloud_key.json - - curl https://sdk.cloud.google.com | bash - - gcloud auth activate-service-account --key-file /tmp/gcloud_key.json - - gcloud docker -a - - docker push $PRIVATE_REGISTRY/$CIRCLE_PROJECT_REPONAME:b$CIRCLE_BUILD_NUM - -notify: - webhooks: - - url: https://cc-slack-proxy.herokuapp.com/circle diff --git a/index.js b/index.js deleted file mode 100644 index ad1a2db..0000000 --- a/index.js +++ /dev/null @@ -1,105 +0,0 @@ -var glob = require('glob'); -var exec = require('child_process').exec; -var fs = require('fs'); -var path = require('path'); - -module.exports = FixMe; -function FixMe() { } - -// Strings to scan for in source -var fixmeStrings = "'(FIXME|TODO|HACK|XXX|BUG)'"; - -var excludeExtensions = [".jpg", ".jpeg", ".png", ".gif"]; - -// Prints properly structured Issue data to STDOUT according to -// Code Climate Engine specification. -var printIssue = function(fileName, lineNum, matchedString){ - var issue = { - "type": "issue", - "check_name": matchedString, - "description": matchedString + " found", - "categories": ["Bug Risk"], - "location":{ - "path": fileName, - "lines": { - "begin": lineNum, - "end": lineNum - } - } - }; - - // Issues must be followed by a null byte - var issueString = JSON.stringify(issue)+"\0"; - console.log(issueString); -} - -var findFixmes = function(file){ - // Prepare the grep string for execution (uses BusyBox grep) - var grepString = "grep -inHwoE " + fixmeStrings + " " + file; - - // Execute grep with the FIXME patterns - exec(grepString, function (error, stdout, stderr) { - var results = stdout.toString(); - - if (results !== ""){ - // Parses grep output - var lines = results.split("\n"); - - lines.forEach(function(line, index, array){ - // grep spits out an extra line that we can ignore - if(index < (array.length-1)){ - // Grep output is colon delimited - var cols = line.split(":"); - - // Remove remnants of container paths for external display - var fileName = cols[0].split("/code/")[1]; - var lineNum = cols[1]; - var matchedString = cols[2]; - - if (matchedString !== undefined){ - printIssue(fileName, parseInt(lineNum), matchedString); - } - } - }) - } - }) -} - -var eligibleFile = function(fp, excludePaths){ - return (excludePaths.indexOf(fp.split("/code/")[1]) < 0) && - !fs.lstatSync(fp).isDirectory() && - (excludeExtensions.indexOf(path.extname(fp)) < 0) -} - -// Uses glob to traverse code directory and find files to analyze, -// excluding files passed in with by CLI config -var fileWalk = function(excludePaths){ - var analysisFiles = []; - var allFiles = glob.sync("/code/**/**", {}); - - allFiles.forEach(function(file, i, a){ - if(eligibleFile(file, excludePaths)){ - analysisFiles.push(file); - } - }); - - return analysisFiles; -} - -FixMe.prototype.runEngine = function(){ - // Check for existence of config.json, parse exclude paths if it exists - if (fs.existsSync("/config.json")) { - var engineConfig = JSON.parse(fs.readFileSync("/config.json")); - var excludePaths = engineConfig.exclude_paths; - } else { - var excludePaths = []; - } - - // Walk /code/ path and find files to analyze - var analysisFiles = fileWalk(excludePaths); - - // Execute main loop and find fixmes in valid files - analysisFiles.forEach(function(f, i, a){ - findFixmes(f); - }); -} diff --git a/lib/fix-me.js b/lib/fix-me.js new file mode 100644 index 0000000..2a06012 --- /dev/null +++ b/lib/fix-me.js @@ -0,0 +1,88 @@ +var readline = require('readline'); +var spawn = require('child_process').spawn; +var fs = require('fs'); + +var DEFAULT_PATHS = ['./']; +var DEFAULT_STRINGS = ['BUG', 'FIXME', 'HACK', 'TODO', 'XXX']; +var GREP_OPTIONS = [ + '--binary-files=without-match', + '--extended-regexp', + '--line-number', + '--only-matching', + '--recursive', + '--with-filename', + '--word-regexp', +]; + +function FixMe(writable) { + this.output = writable || process.stdout; +} + +FixMe.prototype.run = function(engineConfig) { + var paths, strings; + + if (engineConfig) { + paths = engineConfig.include_paths; + } else { + paths = DEFAULT_PATHS; + } + + if (engineConfig && engineConfig.config && engineConfig.config.strings) { + strings = engineConfig.config.strings; + } else { + strings = DEFAULT_STRINGS; + } + + this.find(paths, strings); +}; + +var isItsOwnConfigFile = function(path) { + return path.indexOf(".codeclimate.yml") !== -1; +}; + +var isAYamlComment = function(path, lineNumber) { + var lines = fs.readFileSync(path, "utf8").split("\n"); + var line = lines[lineNumber - 1] || ""; + return line.match(/^\s*#/); +}; + +FixMe.prototype.find = function(paths, strings, callback) { + var pattern = `(${strings.join('|')})`; + var grep = spawn('grep', [...GREP_OPTIONS, pattern, ...paths]); + + readline.createInterface({ input: grep.stdout }).on('line', (line) => { + var parts = line.split(':'); + var path = parts[0].replace(/^\/code\//, ''); + var lineNumber = parseInt(parts[1], 10); + var matchedString = parts[2]; + + if (!path || !lineNumber || !matchedString) { + process.stderr.write("Ignoring malformed output: " + line + "\n"); + return; + } + + if(isItsOwnConfigFile(path) && !isAYamlComment(path, lineNumber)) { return; } + + var issue = { + 'categories': ['Bug Risk'], + 'check_name': matchedString, + 'description': `${matchedString} found`, + 'location': { + 'lines': { + 'begin': lineNumber, + 'end': lineNumber, + }, + 'path': path, + }, + 'type': 'issue', + }; + + this.output.write(JSON.stringify(issue) + '\0'); + }); + + if (callback) { + grep.stdout.on('close', _ => callback()); + } +}; + +module.exports = FixMe; diff --git a/package.json b/package.json new file mode 100644 index 0000000..62dbccd --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "codeclimate-fixme", + "description": "Static analysis tool that finds FIXME, TODO, BUG, etc. comments in your code.", + "version": "0.0.1", + "main": "./lib/fix-me.js", + "devDependencies": { + "chai": "3.4.1", + "mocha": "2.3.3" + }, + "scripts": { + "test": "mocha" + }, + "files": [ + "LICENSE", + "README.md", + "bin", + "lib" + ], + "repository": { + "type": "git", + "url": "https://github.com/codeclimate/codeclimate-fixme.git" + }, + "keywords": [ + "fixme", + "codeclimate" + ], + "engines": { + "node": ">=0.10" + }, + "license": "MIT" +} diff --git a/test/fix-me.js b/test/fix-me.js new file mode 100644 index 0000000..3340c85 --- /dev/null +++ b/test/fix-me.js @@ -0,0 +1,162 @@ +/* global define, it, describe, context */ + +var expect = require('chai').expect; +var FixMe = require('../lib/fix-me.js'); +var IssueBuffer = require('./support/issue_buffer'); + +describe("fixMe", function(){ + describe("#run(engineConfig)", function() { + context('without engine configuration', function() { + it('uses default strings', function(done) { + var engine = new FixMe(); + + engine.find = function(_, strings) { + expect(strings).to.have.members(['BUG', 'FIXME', 'HACK', 'TODO', 'XXX']); + done(); + }; + + engine.run(); + }); + + it('defaults to the current working directory', function(done) { + var engine = new FixMe(); + + engine.find = function(paths) { + expect(paths).to.have.members(['./']); + done(); + }; + + engine.run(); + }); + }); + + it('passes configured include paths', function(done) { + var engine = new FixMe(); + var config = { + include_paths: ['test/fixtures/code/src/code/test.js'], + }; + + engine.find = function(paths) { + expect(paths).to.have.members(['test/fixtures/code/src/code/test.js']); + done(); + }; + + engine.run(config); + }); + + it('passes configured strings', function(done) { + var engine = new FixMe(); + var engineConfig = { + config: { + strings: ['SUP'] + } + }; + + engine.find = function(_, strings) { + expect(strings).to.have.members(['SUP']); + done(); + }; + + engine.run(engineConfig); + }); + + it('ignores .codeclimate.yml, except for comments', function(done) { + var buf = new IssueBuffer(); + var engine = new FixMe(buf); + + engine.find(['test/fixtures/'], ['URGENT'], function() { + var issues = buf.toIssues(); + var issue_paths = issues.map(issue => issue.location.path); + var cc_config_issue = issues.find(issue => issue.location.path === 'test/fixtures/.codeclimate.yml'); + + expect(cc_config_issue).to.exist; + expect(issues.length).to.eq(2); + expect(issue_paths).to.have.members(['test/fixtures/.codeclimate.yml', 'test/fixtures/urgent.js']); + expect(cc_config_issue.location.lines.begin).to.eq(2); + done(); + }); + }); + }); + + describe('#find(paths, strings)', function() { + it('returns issues for instances of the given strings in the given paths', function(done) { + var buf = new IssueBuffer(); + var engine = new FixMe(buf); + + engine.find(['test/fixtures/file.js'], ['TODO', 'SUP'], function() { + var issues = buf.toIssues(); + + expect(issues.length).to.eq(2); + + expect(issues[0].categories).to.have.members(['Bug Risk']); + expect(issues[0].check_name).to.eq('TODO'); + expect(issues[0].description).to.eq('TODO found'); + expect(issues[0].location.lines.begin).to.eq(1); + expect(issues[0].location.lines.end).to.eq(1); + expect(issues[0].location.path).to.eq('test/fixtures/file.js'); + expect(issues[0].type).to.eq('issue'); + + expect(issues[1].categories).to.have.members(['Bug Risk']); + expect(issues[1].check_name).to.eq('SUP'); + expect(issues[1].description).to.eq('SUP found'); + expect(issues[1].location.lines.begin).to.eq(6); + expect(issues[1].location.lines.end).to.eq(6); + expect(issues[1].location.path).to.eq('test/fixtures/file.js'); + expect(issues[1].type).to.eq('issue'); + + done(); + }); + }); + + it('returns relative paths by stripping /code', function(done) { + var buf = new IssueBuffer(); + var engine = new FixMe(buf); + + engine.find(['/code/file.js'], ['TODO'], function() { + expect(buf.toIssues()[0].location.path).to.eq('file.js'); + done(); + }); + }); + + it('matches case sensitively', function(done) { + var buf = new IssueBuffer(); + var engine = new FixMe(buf); + + // Fixture contains both BUG and bug + engine.find(['test/fixtures/case-sensitivity.js'], ['BUG'], function() { + var issues = buf.toIssues(); + + expect(issues.length).to.eq(1); + expect(issues[0].check_name).to.eq('BUG'); + + done(); + }); + }); + + it('only matches whole words', function(done) { + var buf = new IssueBuffer(); + var engine = new FixMe(buf); + + // Fixture contains both FIXME and FIXMESOON + engine.find(['test/fixtures/whole-words.js'], ['FIXME'], function() { + var issues = buf.toIssues(); + + expect(issues.length).to.eq(1); + expect(issues[0].check_name).to.eq('FIXME'); + + done(); + }); + }); + + it('skips binary files', function(done) { + var buf = new IssueBuffer(); + var engine = new FixMe(buf); + + // Fixture contains output from /dev/urandom + engine.find(['test/fixtures/binary.out'], ['.*'], function() { + expect(buf.toIssues()).to.be.empty; + done(); + }); + }); + }); +}); diff --git a/test/fixtures/.codeclimate.yml b/test/fixtures/.codeclimate.yml new file mode 100644 index 0000000..c085efa --- /dev/null +++ b/test/fixtures/.codeclimate.yml @@ -0,0 +1,7 @@ +engines: + # URGENT: enable duplication engine + fixme: + enabled: true + config: + strings: + - URGENT diff --git a/test/fixtures/binary.out b/test/fixtures/binary.out new file mode 100644 index 0000000..d62f904 Binary files /dev/null and b/test/fixtures/binary.out differ diff --git a/test/fixtures/case-sensitivity.js b/test/fixtures/case-sensitivity.js new file mode 100644 index 0000000..43cd030 --- /dev/null +++ b/test/fixtures/case-sensitivity.js @@ -0,0 +1,5 @@ +// BUG: Fix this. +// This ia a bug +function() { + console.log("this is not a bug"); +} diff --git a/test/fixtures/file.js b/test/fixtures/file.js new file mode 100644 index 0000000..1f69e64 --- /dev/null +++ b/test/fixtures/file.js @@ -0,0 +1,9 @@ +// TODO +function() { + console.log("hello world!"); +} + +// SUP: Fix this. +function() { + console.log("hello world!"); +} diff --git a/test/fixtures/urgent.js b/test/fixtures/urgent.js new file mode 100644 index 0000000..ab28040 --- /dev/null +++ b/test/fixtures/urgent.js @@ -0,0 +1,2 @@ +// URGENT: this is busted +console.log("busted"); diff --git a/test/fixtures/whole-words.js b/test/fixtures/whole-words.js new file mode 100644 index 0000000..1b1b96a --- /dev/null +++ b/test/fixtures/whole-words.js @@ -0,0 +1,9 @@ +// FIXMESOON: Fix this. +function() { + console.log("hello world!"); +} + +// FIXME: Fix this, too. +function() { + console.log("hello world! please FIXMESOON"); +} diff --git a/test/support/issue_buffer.js b/test/support/issue_buffer.js new file mode 100644 index 0000000..b1a976e --- /dev/null +++ b/test/support/issue_buffer.js @@ -0,0 +1,24 @@ +var stream = require('stream'); +var util = require('util'); + +function IssueBuffer() { + this._data = ""; + stream.Writable.call(this); +} + +util.inherits(IssueBuffer, stream.Writable); + +IssueBuffer.prototype._write = function(chunk, encoding, done) { + this._data += chunk.toString(); + done(); +}; + +IssueBuffer.prototype.toIssues = function() { + if (this._data.length === 0) { + return []; + } + + return this._data.slice(0, -1).split('\0').map((json) => JSON.parse(json)); +}; + +module.exports = IssueBuffer; diff --git a/test/test.js b/test/test.js deleted file mode 100644 index 4d741a4..0000000 --- a/test/test.js +++ /dev/null @@ -1,6 +0,0 @@ -// FIXME make this do something -function(){ - console.log("!"); -} - -// TODO make this do something too