diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..164d05b --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,53 @@ +version: 2.1 + +jobs: + build: + machine: + docker_layer_caching: true + working_directory: ~/codeclimate/codeclimate-csslint + 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-csslint + 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 b1b2114..42e43fe 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,6 +1,7 @@ engines: - rubocop: + eslint: enabled: true -ratings: - paths: - - "**.rb" + channel: eslint-4 +exclude_paths: + - test/**/* + - node_modules/**/* diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..01d2a07 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +Makefile +node_modules diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..63664eb --- /dev/null +++ b/.eslintrc @@ -0,0 +1,32 @@ +{ + "env": { + "node": true, + "mocha": true, + "es6": true + }, + "rules": { + "block-spacing": 2, + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "comma-dangle": [2, "never"], + "comma-style": [2, "first", { exceptions: {ArrayExpression: true, ObjectExpression: true} }], + "complexity": [2, 6], + "curly": 2, + "eqeqeq": [2, "allow-null"], + "max-statements": [2, 30], + "no-shadow-restricted-names": 2, + "no-undef": 2, + "no-use-before-define": 2, + "radix": 2, + "semi": 2, + "space-infix-ops": 2, + "strict": 0 + }, + "globals": { + "AnalysisView": true, + "PollingView": true, + "Prism": true, + "Spinner": true, + "Timer": true, + "moment": true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/Dockerfile b/Dockerfile index 3007800..4f271fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,26 @@ -FROM alpine:edge +FROM node:alpine +LABEL maintainer="Code Climate " + +RUN adduser -u 9000 -D app WORKDIR /usr/src/app -COPY Gemfile /usr/src/app/ -COPY Gemfile.lock /usr/src/app/ -RUN apk --update add nodejs git ruby ruby-dev ruby-bundler less ruby-nokogiri build-base && \ - bundle install -j 4 && \ - apk del build-base && rm -fr /usr/share/ri +COPY package.json yarn.lock engine.json ./ -RUN npm install -g codeclimate/csslint.git#7a3a6be +RUN yarn install && \ + chown -R app:app ./ && \ + apk add --no-cache --virtual .dev-deps jq && \ + export csslint_version=$(yarn --json list --pattern csslint 2>/dev/null | jq -r '.data.trees[0].name' | cut -d@ -f2) && \ + cat engine.json | jq '.version = .version + "/" + env.csslint_version' > /engine.json && \ + apk del .dev-deps -RUN adduser -u 9000 -D app -USER app +COPY . ./ COPY . /usr/src/app +USER app + +VOLUME /code +WORKDIR /code + CMD ["/usr/src/app/bin/csslint"] diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 76ec3ca..0000000 --- a/Gemfile +++ /dev/null @@ -1,10 +0,0 @@ -source "https://rubygems.org" - -gem 'json' -gem 'nokogiri' -gem "pry" - -group :test do - gem "rake" - gem "rspec" -end diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index a96e4b5..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,47 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - ansi (1.5.0) - ast (2.0.0) - coderay (1.1.0) - diff-lcs (1.2.5) - json (1.8.3) - method_source (0.8.2) - oga (1.0.2) - ast - ruby-ll (~> 2.1) - pry (0.10.1) - coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - rake (10.4.2) - rspec (3.2.0) - rspec-core (~> 3.2.0) - rspec-expectations (~> 3.2.0) - rspec-mocks (~> 3.2.0) - rspec-core (3.2.3) - rspec-support (~> 3.2.0) - rspec-expectations (3.2.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.2.0) - rspec-mocks (3.2.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.2.0) - rspec-support (3.2.2) - ruby-ll (2.1.2) - ansi - ast - slop (3.6.0) - -PLATFORMS - ruby - -DEPENDENCIES - json - oga - pry - rake - rspec - -BUNDLED WITH - 1.10.2 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f3075a2 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.PHONY: image test release + +IMAGE_NAME ?= codeclimate/codeclimate-csslint +RELEASE_REGISTRY ?= codeclimate + +ifndef RELEASE_TAG +override RELEASE_TAG = latest +endif + +image: + docker build -t codeclimate/codeclimate-csslint . + +test: image + docker run --rm codeclimate/codeclimate-csslint sh -c "cd /usr/src/app && npm run test" + +release: + docker tag $(IMAGE_NAME) $(RELEASE_REGISTRY)/codeclimate-csslint:$(RELEASE_TAG) + docker push $(RELEASE_REGISTRY)/codeclimate-csslint:$(RELEASE_TAG) diff --git a/README.md b/README.md index ecb9ff6..a30c216 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,31 @@ -# Code Climate CSSLint Engine +# Try Qlty today, the newest edition of Code Climate Quality. +#### This repository is deprecated and archived. -[![Code Climate](https://codeclimate.com/repos/558419b6e30ba012290173f6/badges/85c90e6df38db8a9492d/gpa.svg)](https://codeclimate.com/repos/558419b6e30ba012290173f6/feed) +This is a repository for a Code Climate Quality plugin which is packaged as a Docker image. -`codeclimate-csslint` is a Code Climate engine that wraps [CSSLint](https://github.com/CSSLint/csslint). You can run it on your command line using the Code Climate CLI, or on our hosted analysis platform. +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. -CSSLint helps point out problems with your CSS code. It does basic syntax checking as well as applying a set of rules that look for problematic patterns or signs of inefficiency. Each rule is pluggable, so you can easily write your own or omit ones you don't want. +As a result, this repository is no longer maintained and has been archived. -### Installation +## Advantages of Qlty plugins +The new Qlty plugins system provides key advantages over the older, Docker-based plugin system: -1. If you haven't already, [install the Code Climate CLI](https://github.com/codeclimate/codeclimate). -2. Run `codeclimate engines:enable csslint`. 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`. +- 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 -### Need help? +## Try out Qlty today free -For help with CSSLint, [check out their documentation](https://github.com/CSSLint/csslint). +[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. -If you're running into a Code Climate issue, first look over this project's [GitHub Issues](https://github.com/codeclimate/codeclimate-csslint/issues), as your question may have already been covered. If not, [go ahead and open a support ticket with us](https://codeclimate.com/help). + - Install Qlty CLI: +` +curl https://qlty.sh | sh # Mac or Linux +` +or ` powershell -c "iwr https://qlty.sh | iex" # Windows` + +[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/csslint b/bin/csslint index 8b2417d..d9c7377 100755 --- a/bin/csslint +++ b/bin/csslint @@ -1,14 +1,10 @@ -#!/usr/bin/env ruby -$LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), "../lib"))) +#!/usr/local/bin/node --expose-gc -require 'cc/engine/csslint' +const fs = require("fs"); +const Engine = require("../lib/csslint"); -if File.exists?("/config.json") - engine_config = JSON.parse(File.read("/config.json")) -else - engine_config = {} -end +const CONFIG_PATH = "/config.json"; +let config = JSON.parse(fs.readFileSync(CONFIG_PATH)); -CC::Engine::CSSlint.new( - directory: "/code", engine_config: engine_config, io: STDOUT -).run +const CODE_DIR = "/code"; +new Engine(CODE_DIR, console, config).run(); diff --git a/bin/csslint.rb b/bin/csslint.rb new file mode 100755 index 0000000..8b2417d --- /dev/null +++ b/bin/csslint.rb @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby +$LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), "../lib"))) + +require 'cc/engine/csslint' + +if File.exists?("/config.json") + engine_config = JSON.parse(File.read("/config.json")) +else + engine_config = {} +end + +CC::Engine::CSSlint.new( + directory: "/code", engine_config: engine_config, io: STDOUT +).run diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 6d06d9d..0000000 --- a/circle.yml +++ /dev/null @@ -1,24 +0,0 @@ -machine: - services: - - docker - environment: - CLOUDSDK_CORE_DISABLE_PROMPTS: 1 - image_name: codeclimate-csslint - -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..6909387 --- /dev/null +++ b/engine.json @@ -0,0 +1,13 @@ +{ + "name": "CSSLint", + "description": "CSSLint is a tool to help point out problems with your CSS code.", + "maintainer": { + "name": "Code Climate", + "email": "hello@codeclimate.com" + }, + "languages": [ + "CSS" + ], + "version": "2.0.0", + "spec_version": "0.3.1" +} diff --git a/lib/cc/engine/csslint.rb b/lib/cc/engine/csslint.rb deleted file mode 100644 index c7a9980..0000000 --- a/lib/cc/engine/csslint.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'nokogiri' -require 'json' - -module CC - module Engine - class CSSlint - def initialize(directory: , io: , engine_config: ) - @directory = directory - @engine_config = engine_config - @io = io - end - - def run - Dir.chdir(@directory) do - results.xpath('//file').each do |file| - path = file['name'].sub(/\A#{@directory}\//, '') - file.xpath('//error').each do |lint| - issue = { - type: "issue", - check_name: lint["source"], - description: lint["message"], - categories: ["Style"], - remediation_points: 500, - location: { - path: path, - positions: { - begin: { - line: lint["line"].to_i, - column: lint["column"].to_i - }, - end: { - line: lint["line"].to_i, - column: lint["column"].to_i - } - } - } - } - - puts("#{issue.to_json}\0") - end - end - end - end - - private - - def results - @results ||= Nokogiri::XML(csslint_xml) - end - - def csslint_xml - exclusions = @engine_config['exclude_paths'] || [] - final_files = files.reject { |f| exclusions.include?(f) } - `csslint --format=checkstyle-xml #{final_files.join(" ")}` - end - - def files - Dir.glob("**/*css") - end - end - end -end diff --git a/lib/check-details.js b/lib/check-details.js new file mode 100644 index 0000000..13f9e47 --- /dev/null +++ b/lib/check-details.js @@ -0,0 +1,44 @@ +// https://github.com/CSSLint/csslint/wiki/Rules +const ALL_RULES = { + "adjoining-classes": "Compatibility", + "box-model": "Bug Risk", + "box-sizing": "Compatibility", + "bulletproof-font-face": "Compatibility", + "compatible-vendor-prefixes": "Compatibility", + "display-property-grouping": "Bug Risk", + "duplicate-background-images": "Performance", + "duplicate-properties": "Bug Risk", + "empty-rules": "Bug Risk", + "fallback-colors": "Compatibility", + "floats": "Clarity", + "font-faces": "Performance", + "font-sizes": "Clarity", + "gradients": "Compatibility", + "ids": "Complexity", + "import": "Performance", + "important": "Complexity", + "known-properties": "Bug Risk", + "overqualified-elements": "Performance", + "parse-error": "Bug Risk", + "regex-selectors": "Performance", + "shorthand": "Performance", + "star-property-hack": "Compatibility", + "text-indent": "Compatibility", + "underscore-property-hack": "Compatibility", + "unique-headings": "Duplication", + "universal-selector": "Performance", + "unqualified-attributes": "Performance", + "vendor-prefix": "Compatibility", + "zero-units": "Performance" +}; + +const DEFAULT_CATEGORY = "Style"; +const DEFAULT_REMEDIATION_POINTS = 50000; + +module.exports = function(check_name) { + let category = ALL_RULES[check_name] || DEFAULT_CATEGORY; + return { + categories: [category], + remediation_points: DEFAULT_REMEDIATION_POINTS + }; +}; diff --git a/lib/codeclimate-formatter.js b/lib/codeclimate-formatter.js new file mode 100644 index 0000000..ed9f03b --- /dev/null +++ b/lib/codeclimate-formatter.js @@ -0,0 +1,78 @@ +"use strict"; + +const checkDetails = require('./check-details'); + +const DEFAULT_IDENTIFIER = "parse-error"; +const forEach = require("csslint/dist/csslint-node").CSSLint.Util.forEach; + +function ruleIdentifier(rule) { + if (!rule || !("id" in rule)) { + return "generic"; + } + return rule.id; +}; + +function reportJSON(filename, report) { + let check_name = report.rule ? ruleIdentifier(report.rule) : DEFAULT_IDENTIFIER; + let details = checkDetails(check_name); + + return JSON.stringify({ + type: "issue", + check_name: check_name, + description: report.message, + categories: details.categories, + remediation_points: details.remediation_points, + location: { + path: filename, + positions: { + begin: { + line: report.line || 1, + column: report.col || 1 + }, + end: { + line: report.line || 1, + column: report.col || 1 + } + } + } + }) + "\x00"; +} + +module.exports = { + // format information + id: "codeclimate", + name: "Code Climate format", + + startFormat: function() { + return ""; + }, + endFormat: function() { + return ""; + }, + + readError: function(filename, message) { + let report = { + type: "error", + line: 1, + col: 1, + message : message + }; + return reportJSON(filename, report); + }, + + formatResults: function(results, filename/*, options*/) { + let reports = results.messages; + let output = []; + + if (reports.length > 0) { + forEach(reports, function (report) { + // ignore rollups for now + if (!report.rollup) { + output.push(reportJSON(filename, report)); + } + }); + } + + return output.join(""); + } +}; diff --git a/lib/csslint.js b/lib/csslint.js new file mode 100644 index 0000000..bf1dffa --- /dev/null +++ b/lib/csslint.js @@ -0,0 +1,120 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const glob = require("glob"); +const CSSLint = require("csslint/dist/csslint-node").CSSLint; +const CodeClimateFormatter = require("./codeclimate-formatter"); + +const DEFAULT_EXTENSIONS = [".css"]; + +CSSLint.addFormatter(CodeClimateFormatter); + +function readFile(filename) { + try { + return fs.readFileSync(filename, "utf-8"); + } catch (ex) { + return ""; + } +} + + +class Analyzer { + constructor(directory, console, config) { + this.directory = directory; + this.console = console; + this.config = config; + } + + run() { + let files = this.expandPaths(this.config.include_paths || ["./"]); + + this.processFiles(files); + } + + + // private ================================================================= + + + print(message) { + this.console.log(message); + } + + processFile(relativeFilePath) { + let input = readFile(path.join(this.directory, relativeFilePath)); + let formatter = CSSLint.getFormatter("codeclimate"); + + if (!input) { + this.print(formatter.readError(relativeFilePath, "Could not read file data. Is the file empty?")); + } else { + let result = CSSLint.verify(input); + let messages = result.messages || []; + let output = formatter.formatResults(result, relativeFilePath); + if (output) { + this.print(output); + } + } + } + + + processFiles(files) { + for (let file of files) { + this.processFile(file); + } + } + + + expandPaths(paths) { + let files = []; + + for (let path of paths) { + let new_files = this.getFiles(path, this.directory); + files = files.concat(new_files); + } + + return files; + } + + getFiles(pathname) { + var files = []; + let full_pathname = path.normalize(path.join(this.directory, pathname)); + let stat; + let base_name = path.basename(pathname); + + try { + stat = fs.statSync(full_pathname); + } catch (ex) { + return []; + } + + if (stat.isFile() && this.extensionsRegExp.test(full_pathname)) { + return [pathname]; + } else if (stat.isDirectory()) { + for (let file of fs.readdirSync(full_pathname)) { + let new_path = path.join(full_pathname, file); + files = files.concat(this.getFiles(path.relative(this.directory, new_path))); + }; + } + + return files; + } + + get extensionsRegExp() { + return RegExp( + (this.config.extensions || DEFAULT_EXTENSIONS). + map(e => e.replace('.', '\\.')). + join("|") + + "$" + ); + } +} + +module.exports = class { + constructor(directory, console, config) { + this.analyzer = new Analyzer(directory, console, config); + } + + run() { + this.analyzer.run(); + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..2c01c33 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "codeclimate-csslint", + "version": "1.0.0", + "description": "Code Climate CSSLint Engine", + "repository": { + "type": "git", + "url": "https://github.com/codeclimate/codeclimate-csslint.git" + }, + "author": "Code Climate", + "license": "MIT", + "dependencies": { + "csslint": "^1.0.5", + "glob": "^7.1.2" + }, + "devDependencies": { + "chai": "^4.1.2", + "mkdirp": "^0.5.1", + "mocha": "^4.0.1", + "sinon": "^4.1.3", + "temp": "^0.8.3" + }, + "scripts": { + "test": "mocha -gc test" + } +} diff --git a/test/check-details-test.js b/test/check-details-test.js new file mode 100644 index 0000000..bb8646b --- /dev/null +++ b/test/check-details-test.js @@ -0,0 +1,18 @@ +const checkDetails = require("../lib/check-details"); +const expect = require("chai").expect; + +describe("Check Details", function() { + it("returns details for customized checks", function() { + let details = checkDetails("import"); + + expect(details.categories).to.deep.equal(["Performance"]); + expect(details.remediation_points).to.eq(50000); + }); + + it("returns defauls for unknown checks", function() { + let details = checkDetails("made-up"); + + expect(details.categories).to.deep.equal(["Style"]); + expect(details.remediation_points).to.eq(50000); + }); +}); diff --git a/test/codeclimate-formatter_test.js b/test/codeclimate-formatter_test.js new file mode 100644 index 0000000..ad7eb10 --- /dev/null +++ b/test/codeclimate-formatter_test.js @@ -0,0 +1,54 @@ +const Formatter = require("../lib/codeclimate-formatter"); +const expect = require("chai").expect; +const CSSLint = require("csslint/dist/csslint-node").CSSLint; + +describe("Code Climate Formatter", function() { + describe(".startFormat", function() { + it("returns a blank string", function() { + expect(Formatter.startFormat()).to.eq(""); + }); + }); + + describe(".endFormat", function() { + it("returns a blank string", function() { + expect(Formatter.endFormat()).to.eq(""); + }); + }); + + describe(".readError", function() { + it("properly serializes a read error", function() { + expect(Formatter.readError("foo.css", "Can not read the file")).to.eq( + '{"type":"issue","check_name":"parse-error","description":"Can not read the file","categories":["Bug Risk"],"remediation_points":50000,"location":{"path":"foo.css","positions":{"begin":{"line":1,"column":1},"end":{"line":1,"column":1}}}}\x00' + ); + }); + }); + + describe(".formatResults", function() { + it("properly serializes reports", function() { + let reports = [ + { + type: "warning", + line: 1, + col: 1, + message: "Don't use adjoining classes.", + evidence: ".im-bad {", + rule: CSSLint.getRules().find( rule => rule.id === "adjoining-classes") + }, + { + type: "warning", + line: 10, + col: 1, + message: "Disallow empty rules", + evidence: ".empty {}", + rule: CSSLint.getRules().find( rule => rule.id === "empty-rules") + } + ]; + + expect(Formatter.formatResults({messages: reports}, "foo.css")).to.eq( + '{"type":"issue","check_name":"adjoining-classes","description":"Don\'t use adjoining classes.","categories":["Compatibility"],"remediation_points":50000,"location":{"path":"foo.css","positions":{"begin":{"line":1,"column":1},"end":{"line":1,"column":1}}}}\x00' + + '{"type":"issue","check_name":"empty-rules","description":"Disallow empty rules","categories":["Bug Risk"],"remediation_points":50000,"location":{"path":"foo.css","positions":{"begin":{"line":10,"column":1},"end":{"line":10,"column":1}}}}\x00' + ); + }); + }); + +}); diff --git a/test/csslint-test.js b/test/csslint-test.js new file mode 100644 index 0000000..8892f37 --- /dev/null +++ b/test/csslint-test.js @@ -0,0 +1,127 @@ +const Engine = require("../lib/csslint"); +const expect = require("chai").expect; +const CSSLint = require("csslint/dist/csslint-node").CSSLint; +const temp = require('temp').track(); +const fs = require("fs"); +const path = require("path"); +const mkdirp = require('mkdirp').sync; + +class FakeConsole { + constructor() { + this.logs = []; + this.warns = []; + } + + get output() { + return this.logs.join("\n"); + } + + + log(str) { + this.logs.push(str); + } + + warn(str) { + console.warn(str); + this.warns.push(str); + } +} + + +function createSourceFile(root, filename, content) { + let dirname = path.dirname(path.join(root, filename)); + if (!fs.existsSync(dirname)) { + mkdirp(dirname); + } + fs.writeFileSync(path.join(root, filename), content); +} + +describe("CSSLint Engine", function() { + beforeEach(function(){ + this.id_selector_content = "#id { color: red; }"; + this.code_dir = temp.mkdirSync("code"); + this.console = new FakeConsole(); + this.lint = new Engine(this.code_dir, this.console, {}); + }); + + it('analyzes *.css files', function() { + createSourceFile(this.code_dir, 'foo.css', this.id_selector_content); + + this.lint.run(); + expect(this.console.output).to.include("Don't use IDs in selectors."); + }); + + it('fails on malformed file', function() { + createSourceFile(this.code_dir, 'foo.css', '�6�'); + + this.lint.run(); + expect(this.console.output).to.include('Unexpected token'); + }); + + it("doesn't analyze *.scss files", function() { + createSourceFile(this.code_dir, 'foo.scss', this.id_selector_content); + + this.lint.run(); + expect(this.console.output).to.eq(''); + }); + + it("only reports issues in the file where they're present", function() { + createSourceFile(this.code_dir, 'bad.css', this.id_selector_content); + createSourceFile(this.code_dir, 'good.css', '.foo { margin: 0 }'); + + this.lint.run(); + expect(this.console.output).to.not.include('good.css'); + }); + + context("with include_paths", function(){ + beforeEach(function() { + let engine_config = { + include_paths: ["included.css", "included_dir/", "config.yml"] + }; + this.lint = new Engine(this.code_dir, this.console, engine_config); + + createSourceFile(this.code_dir, "included.css", this.id_selector_content); + createSourceFile(this.code_dir, "included_dir/file.css", "p { color: blue !important; }"); + createSourceFile(this.code_dir, "included_dir/sub/sub/subdir/file.css", "img { }"); + createSourceFile(this.code_dir, "config.yml", "foo:\n bar: \"baz\""); + createSourceFile(this.code_dir, "not_included.css", "a { outline: none; }"); + }); + + it("includes all mentioned files", function() { + this.lint.run(); + expect(this.console.output).to.include("Don't use IDs in selectors."); + }); + + it("expands directories", function() { + this.lint.run(); + expect(this.console.output).to.include('Use of !important'); + expect(this.console.output).to.include('Rule is empty'); + }); + + it("excludes any unmentioned files", function() { + this.lint.run(); + expect(this.console.output).to.not.include('Outlines should only be modified using :focus'); + }); + + it("only includes CSS files, even when a non-CSS file is directly included", function() { + this.lint.run(); + expect(this.console.output).to.not.include('config.yml'); + }); + }); + + context("with custom extensions", function(){ + beforeEach(function() { + let engine_config = { + extensions: [".fancycss"] + }; + this.lint = new Engine(this.code_dir, this.console, engine_config); + + createSourceFile(this.code_dir, "master.fancycss", this.id_selector_content); + }); + + it("takes into account extensions", function() { + this.lint.run(); + expect(this.console.output).to.include("Don't use IDs in selectors."); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..99a6eac --- /dev/null +++ b/yarn.lock @@ -0,0 +1,272 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +assertion-error@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +browser-stdout@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" + +chai@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c" + dependencies: + assertion-error "^1.0.1" + check-error "^1.0.1" + deep-eql "^3.0.0" + get-func-name "^2.0.0" + pathval "^1.0.0" + type-detect "^4.0.0" + +check-error@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + +clone@~2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb" + +commander@2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +csslint@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/csslint/-/csslint-1.0.5.tgz#19cc3eda322160fd3f7232af1cb2a360e898a2e9" + dependencies: + clone "~2.1.0" + parserlib "~1.1.1" + +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +deep-eql@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" + dependencies: + type-detect "^4.0.0" + +diff@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" + +diff@^3.1.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" + +escape-string-regexp@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +formatio@1.2.0, formatio@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb" + dependencies: + samsam "1.x" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + +glob@7.1.2, glob@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +growl@1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +he@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +just-extend@^1.1.26: + version "1.1.27" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905" + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + +lolex@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" + +lolex@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.3.1.tgz#3d2319894471ea0950ef64692ead2a5318cff362" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +mkdirp@0.5.1, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mocha@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.0.1.tgz#0aee5a95cf69a4618820f5e51fa31717117daf1b" + dependencies: + browser-stdout "1.3.0" + commander "2.11.0" + debug "3.1.0" + diff "3.3.1" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.3" + he "1.1.1" + mkdirp "0.5.1" + supports-color "4.4.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +nise@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.2.0.tgz#079d6cadbbcb12ba30e38f1c999f36ad4d6baa53" + dependencies: + formatio "^1.2.0" + just-extend "^1.1.26" + lolex "^1.6.0" + path-to-regexp "^1.7.0" + text-encoding "^0.6.4" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +os-tmpdir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +parserlib@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/parserlib/-/parserlib-1.1.1.tgz#a64cfa724062434fdfc351c9a4ec2d92b94c06f4" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + +pathval@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" + +rimraf@~2.2.6: + version "2.2.8" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" + +samsam@1.x: + version "1.3.0" + resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" + +sinon@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-4.1.3.tgz#fc599eda47ed9f1a694ce774b94ab44260bd7ac5" + dependencies: + diff "^3.1.0" + formatio "1.2.0" + lodash.get "^4.4.2" + lolex "^2.2.0" + nise "^1.2.0" + supports-color "^4.4.0" + type-detect "^4.0.5" + +supports-color@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + dependencies: + has-flag "^2.0.0" + +supports-color@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + dependencies: + has-flag "^2.0.0" + +temp@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59" + dependencies: + os-tmpdir "^1.0.0" + rimraf "~2.2.6" + +text-encoding@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" + +type-detect@^4.0.0, type-detect@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.5.tgz#d70e5bc81db6de2a381bcaca0c6e0cbdc7635de2" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"