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 0dacb6a..91ab0c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,20 @@ -FROM node +FROM node:6-alpine +LABEL maintainer="Code Climate " -WORKDIR /usr/src/app +WORKDIR /usr/src/app/ -RUN npm install glob +COPY engine.json / +COPY package.json ./ -COPY . /usr/src/app +# 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/WERE_HIRING.md b/WERE_HIRING.md new file mode 100644 index 0000000..bc97549 --- /dev/null +++ b/WERE_HIRING.md @@ -0,0 +1,5 @@ +# Code Climate is Hiring + +Thanks for checking our our CLI. Since you found your way here, you may be interested in working on open source, and building awesome tools for developers. If so, you should check out our open jobs: + +#### http://jobs.codeclimate.com/ 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 c51658a..0000000 --- a/circle.yml +++ /dev/null @@ -1,23 +0,0 @@ -machine: - services: - - docker - environment: - CLOUDSDK_CORE_DISABLE_PROMPTS: 1 - image_name: codeclimate-fixme - -dependencies: - pre: - - 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 $gcloud_account_email --key-file /tmp/gcloud_key.json - - gcloud docker -a - -test: - override: - - docker build -t=$registry_root/$image_name:b$CIRCLE_BUILD_NUM . - -deployment: - registry: - branch: master - commands: - - docker push $registry_root/$image_name:b$CIRCLE_BUILD_NUM diff --git a/engine.json b/engine.json new file mode 100644 index 0000000..a37f3c9 --- /dev/null +++ b/engine.json @@ -0,0 +1,11 @@ +{ + "name": "fixme", + "description": "Discover lurking FIXMEs, BUGs, TODOs, etc. in your code", + "maintainer": { + "name": "Michael R. Bernstein", + "email": "mrb@codeclimate.com" + }, + "test_paths": [ + "/usr/src/app/test/integration/" + ] +} diff --git a/index.js b/index.js deleted file mode 100644 index e73b976..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": "FIXME found", - "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, 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/integration/fixme.rb b/test/integration/fixme.rb new file mode 100644 index 0000000..fa5c42c --- /dev/null +++ b/test/integration/fixme.rb @@ -0,0 +1,41 @@ +##################################################################### +##################################################################### +## Integration tests for this engine must escape uses of the magic +## keywords, otherwise the engine will flag additional issues on the +## lines with the test markers themselves. We use backslash escaping +## to avoid this problem. +##################################################################### +##################################################################### + +# [issue] check_name="F\IXME" description="F\IXME found" category="Bu\g Risk" +# FIXME: This is broken +x = x + +############################ + +foo = $stdin.gets +# [issue] check_name="B\UG" description="B\UG found" category="Bu\g Risk" +# BUG: This should use double equals +if (foo = "foo") + puts "You said foo" +end + +############################ + +foo = $stdin.gets +# [issue] check_name="X\XX" description="X\XX found" category="Bu\g Risk" +# XXX: This should use double equals +if (foo = "foo") + puts "You said foo" +end + +############################ + +# [issue] check_name="T\ODO" description="T\ODO found" category="Bu\g Risk" +# TODO: Add more tests + +############################ + +# [issue] check_name="H\ACK" description="H\ACK found" category="Bu\g Risk" +# HACK +instance_eval "CRAZY STUFF" 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