├── .editorconfig ├── .github ├── CODEOWNERS └── workflows │ └── sync-repo-labels.yml ├── .gitignore ├── .jscsrc ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── Makefile.node ├── README.md ├── bin └── joblint.js ├── bower.json ├── build ├── joblint.js ├── joblint.min.js └── test.js ├── example ├── browser.html ├── oh-dear.txt ├── passing.txt └── realistic.txt ├── lib ├── joblint.js └── rules.js ├── package.json ├── reporter ├── cli.js └── json.js ├── screenshot.png └── test ├── browser └── test.html └── unit ├── lib └── joblint.js └── setup.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | indent_style = spaces 8 | indent_size = 4 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.json] 13 | insert_final_newline = false 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @rowanmanning 2 | -------------------------------------------------------------------------------- /.github/workflows/sync-repo-labels.yml: -------------------------------------------------------------------------------- 1 | on: [issues, pull_request] 2 | name: Sync repo labels 3 | jobs: 4 | sync: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: rowanmanning/github-labels@v1 8 | with: 9 | github-token: ${{ secrets.GITHUB_TOKEN }} 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowDanglingUnderscores": true, 3 | "disallowEmptyBlocks": true, 4 | "disallowImplicitTypeConversion": [ 5 | "binary", 6 | "boolean", 7 | "numeric", 8 | "string" 9 | ], 10 | "disallowMixedSpacesAndTabs": true, 11 | "disallowMultipleSpaces": true, 12 | "disallowMultipleVarDecl": true, 13 | "disallowNewlineBeforeBlockStatements": true, 14 | "disallowQuotedKeysInObjects": true, 15 | "disallowSpaceAfterObjectKeys": true, 16 | "disallowSpaceAfterPrefixUnaryOperators": true, 17 | "disallowSpaceBeforeComma": true, 18 | "disallowSpaceBeforeSemicolon": true, 19 | "disallowSpacesInCallExpression": true, 20 | "disallowTrailingComma": true, 21 | "disallowTrailingWhitespace": true, 22 | "disallowYodaConditions": true, 23 | "maximumLineLength": 100, 24 | "requireBlocksOnNewline": true, 25 | "requireCamelCaseOrUpperCaseIdentifiers": true, 26 | "requireCapitalizedConstructors": true, 27 | "requireCommaBeforeLineBreak": true, 28 | "requireCurlyBraces": true, 29 | "requireDotNotation": true, 30 | "requireFunctionDeclarations": true, 31 | "requireKeywordsOnNewLine": [ 32 | "else" 33 | ], 34 | "requireLineBreakAfterVariableAssignment": true, 35 | "requireLineFeedAtFileEnd": true, 36 | "requireObjectKeysOnNewLine": true, 37 | "requireParenthesesAroundIIFE": true, 38 | "requireSemicolons": true, 39 | "requireSpaceAfterBinaryOperators": true, 40 | "requireSpaceAfterKeywords": true, 41 | "requireSpaceAfterLineComment": true, 42 | "requireSpaceBeforeBinaryOperators": true, 43 | "requireSpaceBeforeBlockStatements": true, 44 | "requireSpaceBeforeObjectValues": true, 45 | "requireSpaceBetweenArguments": true, 46 | "requireSpacesInConditionalExpression": true, 47 | "requireSpacesInForStatement": true, 48 | "requireSpacesInFunction": { 49 | "beforeOpeningRoundBrace": true, 50 | "beforeOpeningCurlyBrace": true 51 | }, 52 | "validateIndentation": 4, 53 | "validateLineBreaks": "LF", 54 | "validateParameterSeparator": ", ", 55 | "validateQuoteMarks": "'", 56 | 57 | "excludeFiles": [ 58 | "build", 59 | "node_modules" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "after": true, 4 | "afterEach": true, 5 | "before": true, 6 | "beforeEach": true, 7 | "describe": true, 8 | "it": true 9 | }, 10 | "latedef": "nofunc", 11 | "maxparams": 3, 12 | "maxdepth": 2, 13 | "maxstatements": 8, 14 | "maxcomplexity": 5, 15 | "node": true, 16 | "strict": true, 17 | "unused": true 18 | } 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | # Build matrix 3 | language: node_js 4 | matrix: 5 | include: 6 | 7 | # Run linter once 8 | - node_js: '7' 9 | env: LINT=true 10 | 11 | # Run tests 12 | - node_js: '0.10' 13 | - node_js: '0.12' 14 | - node_js: '4' 15 | - node_js: '5' 16 | - node_js: '6' 17 | - node_js: '7' 18 | 19 | # Restrict builds on branches 20 | branches: 21 | only: 22 | - master 23 | - /^\d+\.\d+\.\d+$/ 24 | 25 | # Build script 26 | script: 27 | - 'if [ $LINT ]; then make verify; fi' 28 | - 'if [ ! $LINT ]; then make test; fi' 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | ## 2.3.2 (2016-12-02) 5 | 6 | * Add Node.js 7.0.0 support 7 | 8 | ## 2.3.1 (2016-05-02) 9 | 10 | * Add Node.js 6.0.0 support 11 | 12 | ## 2.3.0 (2015-11-08) 13 | 14 | * Add Node.js 5.0.0 support 15 | * Add some additional rule triggers 16 | 17 | ## 2.2.1 (2015-09-13) 18 | 19 | * Add Node.js 4.0.0 support 20 | * Update dependencies 21 | 22 | ## 2.2.0 (2015-07-07) 23 | 24 | * Add rule for "Need to reassure" 25 | * Add "make it rain" to bro terminology 26 | * Add a rule for "Use of derogatory gendered term" 27 | * Add "gay for" to sexualized terms list 28 | * Fix the "top" trigger 29 | 30 | ## 2.1.1 (2015-07-05) 31 | 32 | * Fix some typos in the examples 33 | 34 | ## 2.1.0 (2015-07-05) 35 | 36 | * Fix a bug where Joblint compiled Regular expressions multiple times 37 | * Add a minified version to the build folder 38 | 39 | ## 2.0.0 (2015-07-04) 40 | 41 | * Rewrite and simplify the library 42 | * Change the result format 43 | * Overhaul the reporters 44 | * Simplify some of the rule triggers 45 | * Add browser and Bower support 46 | 47 | ## 1.3.2 (2014-03-11) 48 | 49 | * Fix issues where "competence" was triggering the "compete" rule 50 | 51 | ## 1.3.1 (2014-02-04) 52 | 53 | * Re-add rules which were accidentally removed 54 | * Small rule additions 55 | 56 | ## 1.3.0 (2013-10-21) 57 | 58 | * Add lots of words to existing rules for sexism, tech, bro and bubble 59 | * Add a rule to catch expanded acronyms 60 | * Add a rule to catch sexism with mentions of facial hair 61 | 62 | ## 1.2.1 (2013-10-07) 63 | 64 | * Misc typo/rule fixes 65 | * A few small additions to bubble rules 66 | 67 | ## 1.2.0 (2013-10-03) 68 | 69 | * Big changes to the way rules work: RegExps are now supported (thanks to @Southern) 70 | * New rules for tech fails 71 | * New rules for "visionary" terminology 72 | * Updates to the sexism rules 73 | * Add evidence to messages in the JSON reporter 74 | * Lots of housekeeping and bug fixes 75 | 76 | ## 1.1.0 (2013-10-02) 77 | 78 | * Add a `verbose` option to the command-line tool 79 | * Misc improvements to the command-line tool 80 | * Small bug fixes 81 | 82 | ## 1.0.2 (2013-10-01) 83 | 84 | * A few additions to bro, bubble, and sexism rules 85 | * Bugfixes 86 | 87 | ## 1.0.1 (2013-09-30) 88 | 89 | * Lots of typos fixed 90 | * A few additions to technology rules 91 | * A few additions to sexism rules 92 | 93 | ## 1.0.0 (2013-09-29) 94 | 95 | * Initial release. 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2015, Rowan Manning 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include Makefile.node 2 | 3 | export EXPECTED_COVERAGE := 85 4 | 5 | VERSION=`node -e "process.stdout.write(require('./package.json').version)"` 6 | HOMEPAGE=`node -e "process.stdout.write(require('./package.json').homepage)"` 7 | 8 | all: install ci bundle 9 | 10 | # Bundle client-side JavaScript 11 | bundle: 12 | @echo "/*! Joblint $(VERSION) | $(HOMEPAGE) */" > build/joblint.js 13 | @echo "/*! Joblint $(VERSION) | $(HOMEPAGE) */" > build/joblint.min.js 14 | @browserify ./lib/joblint --standalone joblint >> build/joblint.js 15 | @browserify ./lib/joblint --standalone joblint | uglifyjs >> build/joblint.min.js 16 | @browserify ./test/unit/setup ./test/unit/lib/joblint > build/test.js 17 | @$(TASK_DONE) 18 | -------------------------------------------------------------------------------- /Makefile.node: -------------------------------------------------------------------------------- 1 | # 2 | # Node.js Makefile 3 | # ================ 4 | # 5 | # Do not update this file manually – it's maintained separately on GitHub: 6 | # https://github.com/rowanmanning/makefiles/blob/master/Makefile.node 7 | # 8 | # To update to the latest version, run `make update-makefile`. 9 | # 10 | 11 | 12 | # Meta tasks 13 | # ---------- 14 | 15 | .PHONY: test 16 | 17 | 18 | # Useful variables 19 | # ---------------- 20 | 21 | NPM_BIN = ./node_modules/.bin 22 | export PATH := $(NPM_BIN):$(PATH) 23 | export EXPECTED_COVERAGE := 90 24 | export INTEGRATION_TIMEOUT := 5000 25 | export INTEGRATION_SLOW := 4000 26 | 27 | 28 | # Output helpers 29 | # -------------- 30 | 31 | TASK_DONE = echo "✓ $@ done" 32 | 33 | 34 | # Group tasks 35 | # ----------- 36 | 37 | all: install ci 38 | ci: verify test 39 | 40 | 41 | # Install tasks 42 | # ------------- 43 | 44 | clean: 45 | @git clean -fxd 46 | @$(TASK_DONE) 47 | 48 | install: node_modules 49 | @$(TASK_DONE) 50 | 51 | node_modules: package.json 52 | @npm prune --production=false 53 | @npm install 54 | @$(TASK_DONE) 55 | 56 | 57 | # Verify tasks 58 | # ------------ 59 | 60 | verify: verify-javascript verify-dust verify-spaces 61 | @$(TASK_DONE) 62 | 63 | verify-javascript: verify-eslint verify-jshint verify-jscs 64 | @$(TASK_DONE) 65 | 66 | verify-dust: 67 | @if [ -e .dustmiterc ]; then dustmite --path ./view && $(TASK_DONE); fi 68 | 69 | verify-eslint: 70 | @if [ -e .eslintrc ]; then eslint . && $(TASK_DONE); fi 71 | 72 | verify-jshint: 73 | @if [ -e .jshintrc ]; then jshint . && $(TASK_DONE); fi 74 | 75 | verify-jscs: 76 | @if [ -e .jscsrc ]; then jscs . && $(TASK_DONE); fi 77 | 78 | verify-spaces: 79 | @if [ -e .editorconfig ] && [ -x $(NPM_BIN)/lintspaces ]; then \ 80 | git ls-files | xargs lintspaces -e .editorconfig && $(TASK_DONE); \ 81 | fi 82 | 83 | verify-coverage: 84 | @if [ -d coverage/lcov-report ] && [ -x $(NPM_BIN)/istanbul ]; then \ 85 | istanbul check-coverage --statement $(EXPECTED_COVERAGE) --branch $(EXPECTED_COVERAGE) --function $(EXPECTED_COVERAGE) && $(TASK_DONE); \ 86 | fi 87 | 88 | # Test tasks 89 | # ---------- 90 | 91 | test: test-unit-coverage verify-coverage test-integration 92 | @$(TASK_DONE) 93 | 94 | test-unit: 95 | @if [ -d test/unit ]; then mocha test/unit --recursive && $(TASK_DONE); fi 96 | 97 | test-unit-coverage: 98 | @if [ -d test/unit ]; then \ 99 | if [ -x $(NPM_BIN)/istanbul ]; then \ 100 | istanbul cover $(NPM_BIN)/_mocha -- test/unit --recursive && $(TASK_DONE); \ 101 | else \ 102 | make test-unit; \ 103 | fi \ 104 | fi 105 | 106 | test-integration: 107 | @if [ -d test/integration ]; then mocha test/integration --timeout $(INTEGRATION_TIMEOUT) --slow $(INTEGRATION_SLOW) && $(TASK_DONE); fi 108 | 109 | 110 | # Tooling tasks 111 | # ------------- 112 | 113 | update-makefile: 114 | @curl -s https://raw.githubusercontent.com/rowanmanning/makefiles/master/Makefile.node > Makefile.node 115 | @$(TASK_DONE) 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | > **Warning** 3 | > 4 | > Hiya :wave: Joblint is very much not under active development and shouldn't really be used. It was build in 2013 as a tool to help me analyse a job ads while I was job hunting and it's very naïve in both the way it's implemented and the kinds of issues it highlights. 5 | > 6 | > I don't recommend using Joblint for anything serious, but I'll be leaving it here as a bit of a historic artefact. 7 | 8 | Joblint 9 | ======= 10 | 11 | Test tech job posts for issues with sexism, culture, expectations, and recruiter fails. 12 | 13 | **Writing a job post?** Use Joblint to make your job attractive to a much broader range of candidates and ensure you're not being discriminatory. 14 | **Getting swamped in job posts?** Use Joblint to filter out the bad ones. 15 | 16 | [![NPM version][shield-npm]][info-npm] 17 | [![Node.js version support][shield-node]][info-node] 18 | [![Build status][shield-build]][info-build] 19 | [![Dependencies][shield-dependencies]][info-dependencies] 20 | [![MIT licensed][shield-license]][info-license] 21 | 22 | ```sh 23 | joblint path/to/job-post.txt 24 | ``` 25 | 26 | Joblint output 27 | 28 | 29 | Table Of Contents 30 | ----------------- 31 | 32 | - [Command-Line Interface](#command-line-interface) 33 | - [JavaScript Interface](#javascript-interface) 34 | - [Configuration](#configuration) 35 | - [Writing Rules](#writing-rules) 36 | - [Examples](#examples) 37 | - [Contributing](#contributing) 38 | - [Thanks](#thanks) 39 | - [License](#license) 40 | 41 | 42 | Command-Line Interface 43 | ---------------------- 44 | 45 | Install Joblint globally with [npm][npm]: 46 | 47 | ```sh 48 | npm install -g joblint 49 | ``` 50 | 51 | This installs the `joblint` command-line tool: 52 | 53 | ``` 54 | Usage: joblint [options] 55 | 56 | Options: 57 | 58 | -h, --help output usage information 59 | -V, --version output the version number 60 | -r, --reporter the reporter to use: cli (default), json 61 | -l, --level the level of message to fail on (exit with code 1): error, warning, notice 62 | -p, --pretty output pretty JSON when using the json reporter 63 | ``` 64 | 65 | Run Joblint against a text file: 66 | 67 | ```sh 68 | joblint path/to/job-post.txt 69 | ``` 70 | 71 | Run Joblint against a text file and output JSON results to another file: 72 | 73 | ```sh 74 | joblint --reporter json path/to/job-post.txt > report.json 75 | ``` 76 | 77 | Run Joblint against piped-in input: 78 | 79 | ```sh 80 | echo "This is a job post" | joblint 81 | ``` 82 | 83 | Run Joblint against the clipboard contents: 84 | 85 | ```sh 86 | # OS X 87 | pbpaste | joblint 88 | 89 | # Linux (with xclip installed) 90 | xclip -o | joblint 91 | ``` 92 | 93 | ### Exit Codes 94 | 95 | The command-line tool uses the following exit codes: 96 | 97 | - `0`: joblint ran successfully, and there are no errors 98 | - `1`: there are errors in the job post 99 | 100 | By default, only issues with a type of `error` will exit with a code of `1`. This is configurable with the `--level` flag which can be set to one of the following: 101 | 102 | - `error`: exit with a code of `1` on errors only, exit with a code of `0` on warnings and notices 103 | - `warning`: exit with a code of `1` on errors and warnings, exit with a code of `0` on notices 104 | - `notice`: exit with a code of `1` on errors, warnings, and notices 105 | - `none`: always exit with a code of `0` 106 | 107 | ### Reporters 108 | 109 | The command-line tool can report results in a few different ways using the `--reporter` flag. The built-in reporters are: 110 | 111 | - `cli`: output results in a human-readable format 112 | - `json`: output results as a JSON object 113 | 114 | You can also write and publish your own reporters. Joblint looks for reporters in the core library, your `node_modules` folder, and the current working directory. The first reporter found will be loaded. So with this command: 115 | 116 | ``` 117 | joblint --reporter foo path/to/job-post.txt 118 | ``` 119 | 120 | The following locations will be checked: 121 | 122 | ``` 123 | /reporter/foo 124 | /node_modules/foo 125 | /foo 126 | ``` 127 | 128 | A joblint reporter should export a single function which accepts two arguments: 129 | 130 | - The test results as an object 131 | - The [Commander][commander] program with all its options 132 | 133 | 134 | JavaScript Interface 135 | -------------------- 136 | 137 | Joblint can run in either a web browser or Node.js. The supported versions are: 138 | 139 | - Node.js 0.10.x, 0.12.x, 4.x, 5.x 140 | - Android Browser 2.2+ 141 | - Edge 0.11+ 142 | - Firefox 26+ 143 | - Google Chrome 14+ 144 | - Internet Explorer 9+ 145 | - Safari 5+ 146 | - Safari iOS 4+ 147 | 148 | ### Node.js 149 | 150 | Install Joblint with [npm][npm] or add to your `package.json`: 151 | 152 | ``` 153 | npm install joblint 154 | ``` 155 | 156 | Require Joblint: 157 | 158 | ```js 159 | var joblint = require('joblint'); 160 | ``` 161 | 162 | ### Browser 163 | 164 | Include the built version of Joblint in your page (found in [built/joblint.js](build/joblint.js)): 165 | 166 | ```html 167 | 168 | ``` 169 | 170 | ### Browser (Bower) 171 | 172 | Install Joblint with [Bower][bower] or add to your `bower.json`: 173 | 174 | ``` 175 | bower install joblint 176 | ``` 177 | 178 | ### Running 179 | 180 | Run Joblint on a string: 181 | 182 | ```js 183 | var results = joblint('This is a job post'); 184 | ``` 185 | 186 | The `results` object that gets returned looks like this: 187 | 188 | ```js 189 | { 190 | 191 | // A count of different issue types 192 | counts: { 193 | foo: Number 194 | }, 195 | 196 | // A list of issues with the job post 197 | issues: [ 198 | 199 | { 200 | name: String, // Short name for the rule that was triggered 201 | reason: String, // A longer description of why this rule was triggered 202 | solution: String, // A short description of how to solve this issue 203 | level: String, // error, warning, or notice 204 | increment: { 205 | foo: Number // The amount that each count has been incremented 206 | }, 207 | occurance: String, // The exact occurance of the trigger 208 | position: Number, // The position of the trigger in the input text 209 | context: String // The text directly around the trigger with the trigger replaced by "{{occurance}}" 210 | } 211 | 212 | ] 213 | } 214 | ``` 215 | 216 | You can also configure Joblint on each run. See [Configuration](#configuration) for more information: 217 | 218 | ```js 219 | var results = joblint('This is a job post', { 220 | // options object 221 | }); 222 | ``` 223 | 224 | 225 | Configuration 226 | ------------- 227 | 228 | ### `rules` (array) 229 | 230 | An array of rules which will override the default set. See [Writing Rules](#writing-rules) for more information. 231 | 232 | ```js 233 | joblint('This is a job post', { 234 | rules: [ 235 | // ... 236 | ] 237 | }); 238 | ``` 239 | 240 | 241 | Writing Rules 242 | ------------- 243 | 244 | Writing rules (for your own use, or contributing back to the core library) is fairly easy. You just need to write rule objects with all the required properties: 245 | 246 | ```js 247 | { 248 | name: String, // Short name for the rule 249 | reason: String, // A longer description of why this rule might be triggered 250 | solution: String, // A short description of how to solve the issue 251 | level: String, // error, warning, or notice 252 | increment: { 253 | foo: Number // Increment a counter by an amount. The default set is: culture, realism, recruiter, sexism, tech 254 | }, 255 | triggers: [ 256 | String // An array of trigger words as strings. These words are converted to regular expressions 257 | ] 258 | } 259 | ``` 260 | 261 | Look in [lib/rules.js](lib/rules.js) for existing rules. 262 | 263 | 264 | Examples 265 | -------- 266 | 267 | There are some example job posts that you can test with in the [example directory](example): 268 | 269 | ```sh 270 | joblint example/passing.txt 271 | joblint example/realistic.txt 272 | joblint example/oh-dear.txt 273 | ``` 274 | 275 | 276 | Contributing 277 | ------------ 278 | 279 | To contribute to Joblint, clone this repo locally and commit your code on a separate branch. 280 | 281 | If you're making core library changes please write unit tests for your code, and check that everything works by running the following before opening a pull-request: 282 | 283 | ```sh 284 | make ci 285 | ``` 286 | 287 | 288 | Thanks 289 | ------ 290 | 291 | The following excellent people helped massively with defining the original lint rules: [Ben Darlow](http://www.kapowaz.net/), [Perry Harlock](http://www.phwebs.co.uk/), [Glynn Phillips](http://www.glynnphillips.co.uk/), [Laura Porter](https://twitter.com/laurabygaslight), [Jude Robinson](https://twitter.com/j0000d), [Luke Stavenhagen](https://twitter.com/stavi), [Andrew Walker](https://twitter.com/moddular). 292 | 293 | Also, there are plenty of [great contributors][contrib] to the library. 294 | 295 | 296 | License 297 | ------- 298 | 299 | Joblint is licensed under the [MIT][info-license] license. 300 | Copyright © 2015, Rowan Manning 301 | 302 | 303 | 304 | [bower]: http://bower.io/ 305 | [commander]: https://github.com/tj/commander.js 306 | [contrib]: https://github.com/rowanmanning/joblint/graphs/contributors 307 | [npm]: https://www.npmjs.com/ 308 | 309 | [info-dependencies]: https://gemnasium.com/rowanmanning/joblint 310 | [info-license]: LICENSE 311 | [info-node]: package.json 312 | [info-npm]: https://www.npmjs.com/package/joblint 313 | [info-build]: https://travis-ci.org/rowanmanning/joblint 314 | [shield-dependencies]: https://img.shields.io/gemnasium/rowanmanning/joblint.svg 315 | [shield-license]: https://img.shields.io/badge/license-MIT-blue.svg 316 | [shield-node]: https://img.shields.io/badge/node.js%20support-0.10–7-brightgreen.svg 317 | [shield-npm]: https://img.shields.io/npm/v/joblint.svg 318 | [shield-build]: https://img.shields.io/travis/rowanmanning/joblint/master.svg 319 | -------------------------------------------------------------------------------- /bin/joblint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | var joblint = require('../lib/joblint'); 6 | var path = require('path'); 7 | var pkg = require('../package.json'); 8 | var program = require('commander'); 9 | var reportResult; 10 | 11 | initProgram(); 12 | runProgram(); 13 | 14 | function initProgram () { 15 | program 16 | .version(pkg.version) 17 | .usage('[options] ') 18 | .option( 19 | '-r, --reporter ', 20 | 'the reporter to use: cli (default), json', 21 | 'cli' 22 | ) 23 | .option( 24 | '-l, --level ', 25 | 'the level of message to fail on (exit with code 1): error, warning, notice', 26 | 'error' 27 | ) 28 | .option( 29 | '-p, --pretty', 30 | 'output pretty JSON when using the json reporter' 31 | ) 32 | .parse(process.argv); 33 | reportResult = loadReporter(program.reporter); 34 | } 35 | 36 | function runProgram () { 37 | if (program.args.length > 1) { 38 | program.help(); 39 | } 40 | if (program.args[0]) { 41 | return runProgramOnFile(program.args[0]); 42 | } 43 | runProgramOnStdIn(); 44 | } 45 | 46 | function runProgramOnFile (fileName) { 47 | fs.readFile(fileName, {encoding: 'utf8'}, function (error, data) { 48 | if (error) { 49 | console.error('File "' + fileName + '" could not be found'); 50 | process.exit(1); 51 | } 52 | handleInputSuccess(data); 53 | }); 54 | } 55 | 56 | function runProgramOnStdIn () { 57 | if (isTty(process.stdin)) { 58 | program.help(); 59 | } 60 | captureStdIn(handleInputSuccess); 61 | } 62 | 63 | function handleInputSuccess (data) { 64 | var result = joblint(data); 65 | reportResult(result, program); 66 | if (reportShouldFail(result, program.level)) { 67 | process.exit(1); 68 | } 69 | } 70 | 71 | function loadReporter (name) { 72 | var reporter = requireFirst([ 73 | '../reporter/' + name, 74 | name, 75 | path.join(process.cwd(), name) 76 | ], null); 77 | if (!reporter) { 78 | console.error('Reporter "' + name + '" could not be found'); 79 | process.exit(1); 80 | } 81 | return reporter; 82 | } 83 | 84 | function requireFirst (stack, defaultReturn) { 85 | if (!stack.length) { 86 | return defaultReturn; 87 | } 88 | try { 89 | return require(stack.shift()); 90 | } 91 | catch (error) { 92 | return requireFirst(stack, defaultReturn); 93 | } 94 | } 95 | 96 | function reportShouldFail (result, level) { 97 | if (level === 'none') { 98 | return false; 99 | } 100 | if (level === 'notice') { 101 | return (result.issues.length > 0); 102 | } 103 | if (level === 'warning') { 104 | return (result.issues.filter(isWarningOrError).length > 0); 105 | } 106 | return (result.issues.filter(isError).length > 0); 107 | } 108 | 109 | function isError (result) { 110 | return (result.level === 'error'); 111 | } 112 | 113 | function isWarningOrError (result) { 114 | return (result.level === 'warning' || result.level === 'error'); 115 | } 116 | 117 | function captureStdIn (done) { 118 | var data = ''; 119 | process.stdin.resume(); 120 | process.stdin.on('data', function (chunk) { 121 | data += chunk; 122 | }); 123 | process.stdin.on('end', function () { 124 | done(data); 125 | }); 126 | } 127 | 128 | function isTty (stream) { 129 | return (stream.isTTY === true); 130 | } 131 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joblint", 3 | "version": "2.3.2", 4 | 5 | "description": "Test tech job posts for issues with sexism, culture, expectations, and recruiter fails", 6 | "keywords": [ "job", "lint" ], 7 | "author": "Rowan Manning (http://rowanmanning.com/)", 8 | 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/rowanmanning/joblint.git" 12 | }, 13 | "homepage": "https://github.com/rowanmanning/joblint", 14 | "bugs": "https://github.com/rowanmanning/joblint/issues", 15 | "license": "MIT", 16 | 17 | "moduleType": [ 18 | "amd", 19 | "globals", 20 | "node" 21 | ], 22 | 23 | "main": "./build/joblint.js" 24 | } 25 | -------------------------------------------------------------------------------- /build/joblint.js: -------------------------------------------------------------------------------- 1 | /*! Joblint 2.3.2 | https://github.com/rowanmanning/joblint */ 2 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.joblint = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o b.position) { 116 | return 1; 117 | } 118 | if (a.position < b.position) { 119 | return -1; 120 | } 121 | return 0; 122 | } 123 | 124 | },{"./rules":2,"extend":3}],2:[function(require,module,exports){ 125 | // jscs:disable maximumLineLength 126 | 'use strict'; 127 | 128 | module.exports = [ 129 | 130 | // Use of gendered word 131 | { 132 | name: 'Use of gendered word', 133 | reason: 'Use of gendered words could indicate that you\'re discriminating in favour of a certain gender.', 134 | solution: 'Replace gendered words with gender-neutral alternatives.', 135 | level: 'error', 136 | increment: { 137 | sexism: 1 138 | }, 139 | triggers: [ 140 | 'boys?', 141 | 'bros?', 142 | 'broth(a|er)s?', 143 | 'chicks?', 144 | 'dads?', 145 | 'dudes?', 146 | 'fathers?', 147 | 'females?', 148 | 'gentlem[ae]n', 149 | 'girls?', 150 | 'grandfathers?', 151 | 'grandmas?', 152 | 'grandmothers?', 153 | 'grandpas?', 154 | 'gran', 155 | 'grann(y|ies)', 156 | 'guys?', 157 | 'husbands?', 158 | 'lad(y|ies)?', 159 | 'm[ae]n', 160 | 'm[ou]ms?', 161 | 'males?', 162 | 'momm(y|ies)', 163 | 'mommas?', 164 | 'mothers?', 165 | 'papas?', 166 | 'sist(a|er)s?', 167 | 'wi(fe|ves)', 168 | 'wom[ae]n' 169 | ] 170 | }, 171 | 172 | 173 | // Use of gendered pronoun 174 | { 175 | name: 'Use of gendered pronoun', 176 | reason: 'Use of gendered pronouns indicate that you\'re discriminating in favour of a certain gender, or fail to recognise that gender is not binary.', 177 | solution: 'Replace gendered pronouns with "them" or "they".', 178 | level: 'error', 179 | increment: { 180 | sexism: 1 181 | }, 182 | triggers: [ 183 | 'he|her|him|his|she' 184 | ] 185 | }, 186 | 187 | // Use of derogatory gendered term 188 | { 189 | name: 'Use of derogatory gendered term', 190 | reason: 'When you use derogatory gendered terms, you\'re being discriminatory. These are offensive in a job post.', 191 | solution: 'Remove these words.', 192 | level: 'error', 193 | increment: { 194 | sexism: 2, 195 | culture: 1 196 | }, 197 | triggers: [ 198 | 'bia?tch(es)?', 199 | 'bimbos?', 200 | 'hoes?', 201 | 'hunks?', 202 | 'milfs?', 203 | 'slags?', 204 | 'sluts?', 205 | 'stallions?', 206 | 'studs?' 207 | ] 208 | }, 209 | 210 | 211 | // Mention of facial hair 212 | { 213 | name: 'Mention of facial hair', 214 | reason: 'The use of "grizzled" or "bearded" indicates that you\'re only looking for male developers.', 215 | solution: 'Remove these words.', 216 | level: 'error', 217 | increment: { 218 | sexism: 1 219 | }, 220 | triggers: [ 221 | 'beard(ed|s|y)?', 222 | 'grizzl(ed|y)' 223 | ] 224 | }, 225 | 226 | 227 | // Use of sexualised terms 228 | { 229 | name: 'Use of sexualised terms', 230 | reason: 'Terms like "sexy code" are often used if the person writing a post doesn\'t know what they are talking about or can\'t articulate what good code is. It can also be an indicator of bro culture or sexism.', 231 | solution: 'Remove these words.', 232 | level: 'warning', 233 | increment: { 234 | culture: 1 235 | }, 236 | triggers: [ 237 | 'gay for', 238 | 'sexy', 239 | 'hawt', 240 | 'phat' 241 | ] 242 | }, 243 | 244 | 245 | // Use of bro terminology 246 | { 247 | name: 'Use of bro terminology', 248 | reason: 'Bro culture terminology can really reduce the number of people likely to show interest. It discriminates against anyone who doesn\'t fit into a single gender-specific archetype.', 249 | solution: 'Remove these words.', 250 | level: 'error', 251 | increment: { 252 | culture: 1 253 | }, 254 | triggers: [ 255 | 'bros?', 256 | 'brogramm(er|ers|ing)', 257 | 'crank', 258 | 'crush', 259 | 'dude(bro)?s?', 260 | 'hard[ -]*core', 261 | 'hella', 262 | 'mak(e|ing) it rain', 263 | 'skillz' 264 | ] 265 | }, 266 | 267 | 268 | // Use of dumb job titles 269 | { 270 | name: 'Use of dumb job titles', 271 | reason: 'Referring to tech people as Ninjas or similar devalues the work that they do and shows a lack of respect and professionalism. It\'s also rather cliched and can be an immediate turn-off to many people.', 272 | solution: 'Consider what you\'re really asking for in an applicant and articulate this in the job post.', 273 | level: 'warning', 274 | increment: { 275 | culture: 1, 276 | realism: 1 277 | }, 278 | triggers: [ 279 | 'gurus?', 280 | 'hero(es|ic)?', 281 | 'ninjas?', 282 | 'rock[ -]*stars?', 283 | 'super[ -]*stars?', 284 | 'badass(es)?', 285 | 'BAMF' 286 | ] 287 | }, 288 | 289 | 290 | // Mention of hollow benefits 291 | { 292 | name: 'Mention of hollow benefits', 293 | reason: 'Benefits such as "beer fridge" and "pool table" are not bad in themselves, but their appearance in a job post often disguises the fact that there are few real benefits to working for a company. Be wary of these.', 294 | solution: 'Ensure you\'re outlining real employee benefits if you have them. Don\'t use these as a carrot.', 295 | level: 'warning', 296 | increment: { 297 | culture: 1, 298 | recruiter: 1 299 | }, 300 | triggers: [ 301 | 'ales?', 302 | 'beers?', 303 | 'brewskis?', 304 | 'coffee', 305 | '(foos|fuss)[ -]*ball', 306 | 'happy[ -]*hours?', 307 | 'keg(erator)?s?', 308 | 'lagers?', 309 | 'nerf[ -]*guns?', 310 | 'ping[ -]*pong?', 311 | 'pints?', 312 | 'pizzas?', 313 | 'play\\s*stations?', 314 | 'pool[ -]*table|pool', 315 | 'rock[ -]*walls?', 316 | 'table[ -]*football', 317 | 'table[ -]*tennis', 318 | 'wiis?', 319 | 'xbox(es|s)?', 320 | 'massages?' 321 | ] 322 | }, 323 | 324 | 325 | // Competitive environment 326 | { 327 | name: 'Competitive environment', 328 | reason: 'Competition can be healthy, but for a lot of people a heavily competitive environment can be a strain. You could also potentially be excluding people who have more important outside-of-work commitments, such as a family.', 329 | solution: 'Be wary if you come across as competitive, aim for welcoming and friendly.', 330 | level: 'notice', 331 | increment: { 332 | realism: 1, 333 | recruiter: 1 334 | }, 335 | triggers: [ 336 | 'compete(?!nt|nce|ncy|ncies)', 337 | 'competition', 338 | 'competitive(?! salary| pay)', 339 | 'cutting[ -]edge', 340 | 'fail', 341 | 'fore[ -]*front', 342 | 'super[ -]*stars?', 343 | 'the best', 344 | 'reach the top', 345 | 'top of .{2,8} (game|class)', 346 | 'win' 347 | ] 348 | }, 349 | 350 | 351 | // New starter expectations 352 | { 353 | name: 'New starter expectations', 354 | reason: 'Terms like "hit the ground running" and others can indicate that the person writing a job post is unaware of the time and effort involved in preparing a new starter for work.', 355 | solution: 'Reevaluate the use of these terms.', 356 | level: 'notice', 357 | increment: { 358 | realism: 1 359 | }, 360 | triggers: [ 361 | 'hit[ -]the[ -]ground[ -]running', 362 | 'juggle', 363 | 'tight deadlines?' 364 | ] 365 | }, 366 | 367 | 368 | 369 | // Use of Meritocracy 370 | { 371 | name: 'Use of Meritocracy', 372 | reason: 'The term "meritocracy" is originally a satirical term relating to how we justify our own successes. Unfortunately, it\'s probably impossible to objectively measure merit, so this usually indicates that the company in question rewards people similar to themselves or using specious metrics.', 373 | solution: 'Reevaluate the use of this term.', 374 | level: 'notice', 375 | increment: { 376 | realism: 1 377 | }, 378 | triggers: [ 379 | 'meritocra(cy|cies|tic)' 380 | ] 381 | }, 382 | 383 | 384 | // Use of profanity 385 | { 386 | name: 'Use of profanity', 387 | reason: 'While swearing in the workplace can be OK, you shouldn\'t be using profanity in a job post – it\'s unprofessional.', 388 | solution: 'Remove these words.', 389 | level: 'warning', 390 | increment: { 391 | recruiter: 1 392 | }, 393 | triggers: [ 394 | 'bloody', 395 | 'bugger', 396 | 'cunt', 397 | 'damn', 398 | 'fuck(er|ing)?', 399 | 'piss(ing)?', 400 | 'shit', 401 | 'motherfuck(ers?|ing)' 402 | ] 403 | }, 404 | 405 | 406 | // Use of "visionary" terminology 407 | { 408 | name: 'Use of "visionary" terminology', 409 | reason: 'Terms like "blue sky" and "enlightened" often indicate that a non technical person (perhaps a CEO or stakeholder) has been involved in writing the post. Be down-to-earth, and explain things in plain English.', 410 | solution: 'Reword these phrases, say what you actually mean.', 411 | level: 'warning', 412 | increment: { 413 | culture: 1, 414 | realism: 1 415 | }, 416 | triggers: [ 417 | 'blue[ -]*sk(y|ies)', 418 | 'enlighten(ed|ing)?', 419 | 'green[ -]*fields?', 420 | 'incentivi[sz]e', 421 | 'paradigm', 422 | 'producti[sz]e', 423 | 'reach(ed|ing)? out', 424 | 'synerg(y|ize|ise)', 425 | 'visionar(y|ies)' 426 | ] 427 | }, 428 | 429 | 430 | // Need to reassure 431 | { 432 | name: 'Need to reassure', 433 | reason: 'Something feels off when you need to reassure someone of something that should definitely not be an issue in any workplace.', 434 | solution: 'Reassess the need for these phrases.', 435 | level: 'notice', 436 | increment: { 437 | culture: 1 438 | }, 439 | triggers: [ 440 | 'drama[ -]*free', 441 | 'stress[ -]*free' 442 | ] 443 | }, 444 | 445 | 446 | // Mention of legacy technology 447 | { 448 | name: 'Mention of legacy technology', 449 | reason: 'Legacy technologies can reduce the number of people interested in a job. Sometimes we can\'t avoid this, but extreme legacy tech can often indicate that a company isn\'t willing to move forwards or invest in career development.', 450 | solution: 'If possible (and you\'re being honest), play down the use of this technology.', 451 | level: 'notice', 452 | increment: { 453 | realism: 1, 454 | tech: 1 455 | }, 456 | triggers: [ 457 | 'cobol', 458 | 'cvs', 459 | 'front[ -]*page', 460 | 'rcs', 461 | 'sccs', 462 | 'source[ -]*safe', 463 | 'vb\\s*6', 464 | 'visual[ -]*basic\\s*6', 465 | 'vbscript' 466 | ] 467 | }, 468 | 469 | 470 | // Mention of a development environment 471 | { 472 | name: 'Mention of a development environment', 473 | reason: 'Unless you\'re building in a something which requires a certain development environment (e.g. iOS development and XCode), it shouldn\'t matter which tools a developer decides to use to write code – their output will be better if they are working in a familiar environment.', 474 | solution: 'Don\'t specify a development environment unless absolutely necessary.', 475 | level: 'notice', 476 | increment: { 477 | culture: 1, 478 | tech: 1 479 | }, 480 | triggers: [ 481 | 'atom', 482 | 'bb[ -]*edit', 483 | 'dream[ -]*weaver', 484 | 'eclipse', 485 | 'emacs', 486 | 'net[ -]*beans', 487 | 'note[ -]*pad', 488 | 'sublime[ -]*text', 489 | 'text[ -]*wrangler', 490 | 'text[ -]*mate', 491 | 'vim?', 492 | 'visual[ -]*studio' 493 | ] 494 | }, 495 | 496 | 497 | // Use of expanded acronyms 498 | { 499 | name: 'Use of expanded acronyms', 500 | reason: 'Tech people know their acronyms; you come across as not very tech-savvy if you expand them.', 501 | solution: 'Use acronyms in place of these words.', 502 | level: 'warning', 503 | increment: { 504 | recruiter: 1, 505 | tech: 1 506 | }, 507 | triggers: [ 508 | 'cascading[ -]?style[ -]?sheets', 509 | 'hyper[ -]?text([ -]?mark[ -]?up([ -]?language)?)?' 510 | ] 511 | }, 512 | 513 | 514 | // Java script? 515 | { 516 | name: 'Java script?', 517 | reason: 'JavaScript is one word. You write JavaScript, not javascripts or java script.', 518 | solution: 'Replace this word with "JavaScript".', 519 | level: 'error', 520 | increment: { 521 | recruiter: 1 522 | }, 523 | triggers: [ 524 | 'java[ -]script|java[ -]*scripts' 525 | ] 526 | }, 527 | 528 | 529 | // Ruby on Rail? 530 | { 531 | name: 'Ruby on Rail?', 532 | reason: 'Ruby On Rails is plural – there is more than one rail.', 533 | solution: 'Replace this with "Ruby on Rails".', 534 | level: 'error', 535 | increment: { 536 | recruiter: 1 537 | }, 538 | triggers: [ 539 | 'ruby on rail' 540 | ] 541 | } 542 | 543 | ]; 544 | 545 | },{}],3:[function(require,module,exports){ 546 | 'use strict'; 547 | 548 | var hasOwn = Object.prototype.hasOwnProperty; 549 | var toStr = Object.prototype.toString; 550 | 551 | var isArray = function isArray(arr) { 552 | if (typeof Array.isArray === 'function') { 553 | return Array.isArray(arr); 554 | } 555 | 556 | return toStr.call(arr) === '[object Array]'; 557 | }; 558 | 559 | var isPlainObject = function isPlainObject(obj) { 560 | if (!obj || toStr.call(obj) !== '[object Object]') { 561 | return false; 562 | } 563 | 564 | var hasOwnConstructor = hasOwn.call(obj, 'constructor'); 565 | var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); 566 | // Not own constructor property must be Object 567 | if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) { 568 | return false; 569 | } 570 | 571 | // Own properties are enumerated firstly, so to speed up, 572 | // if last one is own, then all properties are own. 573 | var key; 574 | for (key in obj) {/**/} 575 | 576 | return typeof key === 'undefined' || hasOwn.call(obj, key); 577 | }; 578 | 579 | module.exports = function extend() { 580 | var options, name, src, copy, copyIsArray, clone, 581 | target = arguments[0], 582 | i = 1, 583 | length = arguments.length, 584 | deep = false; 585 | 586 | // Handle a deep copy situation 587 | if (typeof target === 'boolean') { 588 | deep = target; 589 | target = arguments[1] || {}; 590 | // skip the boolean and the target 591 | i = 2; 592 | } else if ((typeof target !== 'object' && typeof target !== 'function') || target == null) { 593 | target = {}; 594 | } 595 | 596 | for (; i < length; ++i) { 597 | options = arguments[i]; 598 | // Only deal with non-null/undefined values 599 | if (options != null) { 600 | // Extend the base object 601 | for (name in options) { 602 | src = target[name]; 603 | copy = options[name]; 604 | 605 | // Prevent never-ending loop 606 | if (target !== copy) { 607 | // Recurse if we're merging plain objects or arrays 608 | if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) { 609 | if (copyIsArray) { 610 | copyIsArray = false; 611 | clone = src && isArray(src) ? src : []; 612 | } else { 613 | clone = src && isPlainObject(src) ? src : {}; 614 | } 615 | 616 | // Never move original objects, clone them 617 | target[name] = extend(deep, clone, copy); 618 | 619 | // Don't bring in undefined values 620 | } else if (typeof copy !== 'undefined') { 621 | target[name] = copy; 622 | } 623 | } 624 | } 625 | } 626 | } 627 | 628 | // Return the modified object 629 | return target; 630 | }; 631 | 632 | 633 | },{}]},{},[1])(1) 634 | }); -------------------------------------------------------------------------------- /build/joblint.min.js: -------------------------------------------------------------------------------- 1 | /*! Joblint 2.3.2 | https://github.com/rowanmanning/joblint */ 2 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.joblint=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;ob.position){return 1}if(a.position 2 | 3 | 4 | 5 | Joblint Browser Example 6 | 7 | 8 | 9 |

Look in the console!

10 | 11 | 12 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /example/oh-dear.txt: -------------------------------------------------------------------------------- 1 | 2 | Sup. 3 | 4 | We'd like to hire a fucking awesome java script dude, please. A proper web ninja! 5 | If you're good at javascript then please apply and we can crush code together! 6 | 7 | Our site is damn sexy, it was all built with MS Frontpage originally but now we use Dreamweaver mostly. It's important to us that you're at the top of your game, we want to feel enlightened whenever we read your code. 8 | 9 | We'd also like candidates to be able to turn their hand to a little VBScript if possible. He should be able to hit the ground running – we're a cutting-edge, meritocratic company so we can't afford to take on dead-weight. 10 | 11 | Our benefits include a pool table, a fully-stocked beer fridge, and a drama-free environment – we like to reward our heroic dev team properly! 12 | 13 | Call 01234567890 to apply. 14 | Candidates with rad beards get extra credit! 15 | -------------------------------------------------------------------------------- /example/passing.txt: -------------------------------------------------------------------------------- 1 | 2 | Hi. 3 | 4 | We'd like to hire an excellent JavaScript developer, please. 5 | If you're good at JavaScript then please apply. 6 | 7 | Thank you. 8 | -------------------------------------------------------------------------------- /example/realistic.txt: -------------------------------------------------------------------------------- 1 | 2 | Hi. 3 | 4 | We'd like to hire an excellent java script guy, please. 5 | If you're good at javascript then please apply. 6 | 7 | Thank you. 8 | -------------------------------------------------------------------------------- /lib/joblint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var extend = require('extend'); 4 | 5 | module.exports = joblint; 6 | module.exports.defaults = { 7 | rules: require('./rules') 8 | }; 9 | 10 | function joblint (text, options) { 11 | options = defaultOptions(options); 12 | var result = { 13 | counts: {}, 14 | issues: [] 15 | }; 16 | options.rules.forEach(function (rule) { 17 | rule.triggers.forEach(function (trigger) { 18 | var match; 19 | while ((match = trigger.exec(text)) !== null) { 20 | incrementKeys(rule.increment, result.counts); 21 | result.issues.push(buildIssueFromMatch(match, rule)); 22 | } 23 | }); 24 | }); 25 | Object.keys(result.counts).forEach(function (key) { 26 | result.counts[key] = Math.max(result.counts[key], 0); 27 | }); 28 | result.issues = result.issues.sort(sortByPosition); 29 | return result; 30 | } 31 | 32 | function defaultOptions (options) { 33 | options = extend({}, module.exports.defaults, options); 34 | options.rules = buildRules(options.rules); 35 | return options; 36 | } 37 | 38 | function buildRules (rules) { 39 | return rules.map(buildRule); 40 | } 41 | 42 | function buildRule (rule) { 43 | rule = extend(true, {}, rule); 44 | rule.increment = rule.increment || {}; 45 | rule.triggers = rule.triggers.map(function (trigger) { 46 | return new RegExp('\\b(' + trigger + ')\\b', 'gim'); 47 | }); 48 | return rule; 49 | } 50 | 51 | function incrementKeys (amounts, store) { 52 | Object.keys(amounts).forEach(function (key) { 53 | if (!store[key]) { 54 | store[key] = 0; 55 | } 56 | store[key] += amounts[key]; 57 | }); 58 | } 59 | 60 | function buildIssueFromMatch (match, rule) { 61 | var issue = { 62 | name: rule.name, 63 | reason: rule.reason, 64 | solution: rule.solution, 65 | level: rule.level, 66 | increment: rule.increment, 67 | occurance: match[1], 68 | position: match.index 69 | }; 70 | issue.context = buildIssueContext(match.input, issue.occurance, issue.position); 71 | return issue; 72 | } 73 | 74 | function buildIssueContext (input, occurance, position) { 75 | 76 | var context = '{{occurance}}'; 77 | 78 | input 79 | .substr(0, position) 80 | .split(/[\r\n]+/) 81 | .pop() 82 | .replace(/\s+/g, ' ') 83 | .split(/(\s+)/) 84 | .reverse() 85 | .forEach(function (word) { 86 | if (context.length < 32) { 87 | context = word + context; 88 | } 89 | else if (!/^…/.test(context)) { 90 | context = '…' + context.trim(); 91 | } 92 | }); 93 | 94 | input 95 | .substr(position + occurance.length) 96 | .split(/[\r\n]+/) 97 | .shift() 98 | .replace(/\s+/g, ' ') 99 | .split(/(\s+)/) 100 | .forEach(function (word) { 101 | if (context.length < 52) { 102 | context += word; 103 | } 104 | else if (!/…$/.test(context)) { 105 | context = context.trim() + '…'; 106 | } 107 | }); 108 | 109 | return context.trim(); 110 | } 111 | 112 | function sortByPosition (a, b) { 113 | if (a.position > b.position) { 114 | return 1; 115 | } 116 | if (a.position < b.position) { 117 | return -1; 118 | } 119 | return 0; 120 | } 121 | -------------------------------------------------------------------------------- /lib/rules.js: -------------------------------------------------------------------------------- 1 | // jscs:disable maximumLineLength 2 | 'use strict'; 3 | 4 | module.exports = [ 5 | 6 | // Use of gendered word 7 | { 8 | name: 'Use of gendered word', 9 | reason: 'Use of gendered words could indicate that you\'re discriminating in favour of a certain gender.', 10 | solution: 'Replace gendered words with gender-neutral alternatives.', 11 | level: 'error', 12 | increment: { 13 | sexism: 1 14 | }, 15 | triggers: [ 16 | 'boys?', 17 | 'bros?', 18 | 'broth(a|er)s?', 19 | 'chicks?', 20 | 'dads?', 21 | 'dudes?', 22 | 'fathers?', 23 | 'females?', 24 | 'gentlem[ae]n', 25 | 'girls?', 26 | 'grandfathers?', 27 | 'grandmas?', 28 | 'grandmothers?', 29 | 'grandpas?', 30 | 'gran', 31 | 'grann(y|ies)', 32 | 'guys?', 33 | 'husbands?', 34 | 'lad(y|ies)?', 35 | 'm[ae]n', 36 | 'm[ou]ms?', 37 | 'males?', 38 | 'momm(y|ies)', 39 | 'mommas?', 40 | 'mothers?', 41 | 'papas?', 42 | 'sist(a|er)s?', 43 | 'wi(fe|ves)', 44 | 'wom[ae]n' 45 | ] 46 | }, 47 | 48 | 49 | // Use of gendered pronoun 50 | { 51 | name: 'Use of gendered pronoun', 52 | reason: 'Use of gendered pronouns indicate that you\'re discriminating in favour of a certain gender, or fail to recognise that gender is not binary.', 53 | solution: 'Replace gendered pronouns with "them" or "they".', 54 | level: 'error', 55 | increment: { 56 | sexism: 1 57 | }, 58 | triggers: [ 59 | 'he|her|him|his|she' 60 | ] 61 | }, 62 | 63 | // Use of derogatory gendered term 64 | { 65 | name: 'Use of derogatory gendered term', 66 | reason: 'When you use derogatory gendered terms, you\'re being discriminatory. These are offensive in a job post.', 67 | solution: 'Remove these words.', 68 | level: 'error', 69 | increment: { 70 | sexism: 2, 71 | culture: 1 72 | }, 73 | triggers: [ 74 | 'bia?tch(es)?', 75 | 'bimbos?', 76 | 'hoes?', 77 | 'hunks?', 78 | 'milfs?', 79 | 'slags?', 80 | 'sluts?', 81 | 'stallions?', 82 | 'studs?' 83 | ] 84 | }, 85 | 86 | 87 | // Mention of facial hair 88 | { 89 | name: 'Mention of facial hair', 90 | reason: 'The use of "grizzled" or "bearded" indicates that you\'re only looking for male developers.', 91 | solution: 'Remove these words.', 92 | level: 'error', 93 | increment: { 94 | sexism: 1 95 | }, 96 | triggers: [ 97 | 'beard(ed|s|y)?', 98 | 'grizzl(ed|y)' 99 | ] 100 | }, 101 | 102 | 103 | // Use of sexualised terms 104 | { 105 | name: 'Use of sexualised terms', 106 | reason: 'Terms like "sexy code" are often used if the person writing a post doesn\'t know what they are talking about or can\'t articulate what good code is. It can also be an indicator of bro culture or sexism.', 107 | solution: 'Remove these words.', 108 | level: 'warning', 109 | increment: { 110 | culture: 1 111 | }, 112 | triggers: [ 113 | 'gay for', 114 | 'sexy', 115 | 'hawt', 116 | 'phat' 117 | ] 118 | }, 119 | 120 | 121 | // Use of bro terminology 122 | { 123 | name: 'Use of bro terminology', 124 | reason: 'Bro culture terminology can really reduce the number of people likely to show interest. It discriminates against anyone who doesn\'t fit into a single gender-specific archetype.', 125 | solution: 'Remove these words.', 126 | level: 'error', 127 | increment: { 128 | culture: 1 129 | }, 130 | triggers: [ 131 | 'bros?', 132 | 'brogramm(er|ers|ing)', 133 | 'crank', 134 | 'crush', 135 | 'dude(bro)?s?', 136 | 'hard[ -]*core', 137 | 'hella', 138 | 'mak(e|ing) it rain', 139 | 'skillz' 140 | ] 141 | }, 142 | 143 | 144 | // Use of dumb job titles 145 | { 146 | name: 'Use of dumb job titles', 147 | reason: 'Referring to tech people as Ninjas or similar devalues the work that they do and shows a lack of respect and professionalism. It\'s also rather cliched and can be an immediate turn-off to many people.', 148 | solution: 'Consider what you\'re really asking for in an applicant and articulate this in the job post.', 149 | level: 'warning', 150 | increment: { 151 | culture: 1, 152 | realism: 1 153 | }, 154 | triggers: [ 155 | 'gurus?', 156 | 'hero(es|ic)?', 157 | 'ninjas?', 158 | 'rock[ -]*stars?', 159 | 'super[ -]*stars?', 160 | 'badass(es)?', 161 | 'BAMF' 162 | ] 163 | }, 164 | 165 | 166 | // Mention of hollow benefits 167 | { 168 | name: 'Mention of hollow benefits', 169 | reason: 'Benefits such as "beer fridge" and "pool table" are not bad in themselves, but their appearance in a job post often disguises the fact that there are few real benefits to working for a company. Be wary of these.', 170 | solution: 'Ensure you\'re outlining real employee benefits if you have them. Don\'t use these as a carrot.', 171 | level: 'warning', 172 | increment: { 173 | culture: 1, 174 | recruiter: 1 175 | }, 176 | triggers: [ 177 | 'ales?', 178 | 'beers?', 179 | 'brewskis?', 180 | 'coffee', 181 | '(foos|fuss)[ -]*ball', 182 | 'happy[ -]*hours?', 183 | 'keg(erator)?s?', 184 | 'lagers?', 185 | 'nerf[ -]*guns?', 186 | 'ping[ -]*pong?', 187 | 'pints?', 188 | 'pizzas?', 189 | 'play\\s*stations?', 190 | 'pool[ -]*table|pool', 191 | 'rock[ -]*walls?', 192 | 'table[ -]*football', 193 | 'table[ -]*tennis', 194 | 'wiis?', 195 | 'xbox(es|s)?', 196 | 'massages?' 197 | ] 198 | }, 199 | 200 | 201 | // Competitive environment 202 | { 203 | name: 'Competitive environment', 204 | reason: 'Competition can be healthy, but for a lot of people a heavily competitive environment can be a strain. You could also potentially be excluding people who have more important outside-of-work commitments, such as a family.', 205 | solution: 'Be wary if you come across as competitive, aim for welcoming and friendly.', 206 | level: 'notice', 207 | increment: { 208 | realism: 1, 209 | recruiter: 1 210 | }, 211 | triggers: [ 212 | 'compete(?!nt|nce|ncy|ncies)', 213 | 'competition', 214 | 'competitive(?! salary| pay)', 215 | 'cutting[ -]edge', 216 | 'fail', 217 | 'fore[ -]*front', 218 | 'super[ -]*stars?', 219 | 'the best', 220 | 'reach the top', 221 | 'top of .{2,8} (game|class)', 222 | 'win' 223 | ] 224 | }, 225 | 226 | 227 | // New starter expectations 228 | { 229 | name: 'New starter expectations', 230 | reason: 'Terms like "hit the ground running" and others can indicate that the person writing a job post is unaware of the time and effort involved in preparing a new starter for work.', 231 | solution: 'Reevaluate the use of these terms.', 232 | level: 'notice', 233 | increment: { 234 | realism: 1 235 | }, 236 | triggers: [ 237 | 'hit[ -]the[ -]ground[ -]running', 238 | 'juggle', 239 | 'tight deadlines?' 240 | ] 241 | }, 242 | 243 | 244 | 245 | // Use of Meritocracy 246 | { 247 | name: 'Use of Meritocracy', 248 | reason: 'The term "meritocracy" is originally a satirical term relating to how we justify our own successes. Unfortunately, it\'s probably impossible to objectively measure merit, so this usually indicates that the company in question rewards people similar to themselves or using specious metrics.', 249 | solution: 'Reevaluate the use of this term.', 250 | level: 'notice', 251 | increment: { 252 | realism: 1 253 | }, 254 | triggers: [ 255 | 'meritocra(cy|cies|tic)' 256 | ] 257 | }, 258 | 259 | 260 | // Use of profanity 261 | { 262 | name: 'Use of profanity', 263 | reason: 'While swearing in the workplace can be OK, you shouldn\'t be using profanity in a job post – it\'s unprofessional.', 264 | solution: 'Remove these words.', 265 | level: 'warning', 266 | increment: { 267 | recruiter: 1 268 | }, 269 | triggers: [ 270 | 'bloody', 271 | 'bugger', 272 | 'cunt', 273 | 'damn', 274 | 'fuck(er|ing)?', 275 | 'piss(ing)?', 276 | 'shit', 277 | 'motherfuck(ers?|ing)' 278 | ] 279 | }, 280 | 281 | 282 | // Use of "visionary" terminology 283 | { 284 | name: 'Use of "visionary" terminology', 285 | reason: 'Terms like "blue sky" and "enlightened" often indicate that a non technical person (perhaps a CEO or stakeholder) has been involved in writing the post. Be down-to-earth, and explain things in plain English.', 286 | solution: 'Reword these phrases, say what you actually mean.', 287 | level: 'warning', 288 | increment: { 289 | culture: 1, 290 | realism: 1 291 | }, 292 | triggers: [ 293 | 'blue[ -]*sk(y|ies)', 294 | 'enlighten(ed|ing)?', 295 | 'green[ -]*fields?', 296 | 'incentivi[sz]e', 297 | 'paradigm', 298 | 'producti[sz]e', 299 | 'reach(ed|ing)? out', 300 | 'synerg(y|ize|ise)', 301 | 'visionar(y|ies)' 302 | ] 303 | }, 304 | 305 | 306 | // Need to reassure 307 | { 308 | name: 'Need to reassure', 309 | reason: 'Something feels off when you need to reassure someone of something that should definitely not be an issue in any workplace.', 310 | solution: 'Reassess the need for these phrases.', 311 | level: 'notice', 312 | increment: { 313 | culture: 1 314 | }, 315 | triggers: [ 316 | 'drama[ -]*free', 317 | 'stress[ -]*free' 318 | ] 319 | }, 320 | 321 | 322 | // Mention of legacy technology 323 | { 324 | name: 'Mention of legacy technology', 325 | reason: 'Legacy technologies can reduce the number of people interested in a job. Sometimes we can\'t avoid this, but extreme legacy tech can often indicate that a company isn\'t willing to move forwards or invest in career development.', 326 | solution: 'If possible (and you\'re being honest), play down the use of this technology.', 327 | level: 'notice', 328 | increment: { 329 | realism: 1, 330 | tech: 1 331 | }, 332 | triggers: [ 333 | 'cobol', 334 | 'cvs', 335 | 'front[ -]*page', 336 | 'rcs', 337 | 'sccs', 338 | 'source[ -]*safe', 339 | 'vb\\s*6', 340 | 'visual[ -]*basic\\s*6', 341 | 'vbscript' 342 | ] 343 | }, 344 | 345 | 346 | // Mention of a development environment 347 | { 348 | name: 'Mention of a development environment', 349 | reason: 'Unless you\'re building in a something which requires a certain development environment (e.g. iOS development and XCode), it shouldn\'t matter which tools a developer decides to use to write code – their output will be better if they are working in a familiar environment.', 350 | solution: 'Don\'t specify a development environment unless absolutely necessary.', 351 | level: 'notice', 352 | increment: { 353 | culture: 1, 354 | tech: 1 355 | }, 356 | triggers: [ 357 | 'atom', 358 | 'bb[ -]*edit', 359 | 'dream[ -]*weaver', 360 | 'eclipse', 361 | 'emacs', 362 | 'net[ -]*beans', 363 | 'note[ -]*pad', 364 | 'sublime[ -]*text', 365 | 'text[ -]*wrangler', 366 | 'text[ -]*mate', 367 | 'vim?', 368 | 'visual[ -]*studio' 369 | ] 370 | }, 371 | 372 | 373 | // Use of expanded acronyms 374 | { 375 | name: 'Use of expanded acronyms', 376 | reason: 'Tech people know their acronyms; you come across as not very tech-savvy if you expand them.', 377 | solution: 'Use acronyms in place of these words.', 378 | level: 'warning', 379 | increment: { 380 | recruiter: 1, 381 | tech: 1 382 | }, 383 | triggers: [ 384 | 'cascading[ -]?style[ -]?sheets', 385 | 'hyper[ -]?text([ -]?mark[ -]?up([ -]?language)?)?' 386 | ] 387 | }, 388 | 389 | 390 | // Java script? 391 | { 392 | name: 'Java script?', 393 | reason: 'JavaScript is one word. You write JavaScript, not javascripts or java script.', 394 | solution: 'Replace this word with "JavaScript".', 395 | level: 'error', 396 | increment: { 397 | recruiter: 1 398 | }, 399 | triggers: [ 400 | 'java[ -]script|java[ -]*scripts' 401 | ] 402 | }, 403 | 404 | 405 | // Ruby on Rail? 406 | { 407 | name: 'Ruby on Rail?', 408 | reason: 'Ruby On Rails is plural – there is more than one rail.', 409 | solution: 'Replace this with "Ruby on Rails".', 410 | level: 'error', 411 | increment: { 412 | recruiter: 1 413 | }, 414 | triggers: [ 415 | 'ruby on rail' 416 | ] 417 | } 418 | 419 | ]; 420 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joblint", 3 | "version": "2.3.2", 4 | "description": "Test tech job posts for issues with sexism, culture, expectations, and recruiter fails", 5 | "keywords": [ 6 | "job", 7 | "lint" 8 | ], 9 | "author": "Rowan Manning (http://rowanmanning.com/)", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/rowanmanning/joblint.git" 13 | }, 14 | "homepage": "https://github.com/rowanmanning/joblint", 15 | "bugs": "https://github.com/rowanmanning/joblint/issues", 16 | "license": "MIT", 17 | "engines": { 18 | "node": ">=0.10" 19 | }, 20 | "dependencies": { 21 | "chalk": "1.1", 22 | "commander": "~2.8", 23 | "extend": "~3.0", 24 | "pad-component": "0.0.1", 25 | "wordwrap": "~1.0" 26 | }, 27 | "devDependencies": { 28 | "browserify": "^11", 29 | "jscs": "^2", 30 | "jshint": "^2", 31 | "mocha": "^2", 32 | "mockery": "~1.4", 33 | "proclaim": "^3", 34 | "sinon": "^1", 35 | "uglify-js": "^2" 36 | }, 37 | "main": "./lib/joblint.js", 38 | "bin": { 39 | "joblint": "./bin/joblint.js" 40 | }, 41 | "scripts": { 42 | "test": "make ci" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /reporter/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chalk = require('chalk'); 4 | var pad = require('pad-component'); 5 | var wrap = require('wordwrap')(4, Math.min(process.stdout.columns - 4, 76)); 6 | 7 | module.exports = report; 8 | 9 | function report (result) { 10 | console.log('\n%s', chalk.cyan.underline('Joblint')); 11 | if (Object.keys(result.counts).length) { 12 | console.log('\n%s', chalk.grey('Issue tally:')); 13 | reportTallyChart(result.counts); 14 | } 15 | if (result.issues.length) { 16 | result.issues.forEach(reportIssue); 17 | } 18 | else { 19 | console.log('\n' + chalk.green('✔ No issues found!')); 20 | } 21 | console.log(''); 22 | } 23 | 24 | function reportTallyChart (counts) { 25 | var labels = Object.keys(counts); 26 | var values = labels.map(function (label) { 27 | return counts[label]; 28 | }); 29 | var bars = values.map(function (count) { 30 | return pad.right('', count, '█'); 31 | }); 32 | bars = bars.map(padEach(getLongest(bars))); 33 | labels.map(padEach(getLongest(labels))).forEach(function (label, index) { 34 | console.log( 35 | capitalizeFirstLetter(label), 36 | chalk.grey(' |') + chalk.yellow(bars[index]), 37 | chalk.grey(' (' + values[index] + ')') 38 | ); 39 | }); 40 | } 41 | 42 | function padEach (length, character) { 43 | return function (value) { 44 | return pad.right(value, length, character); 45 | }; 46 | } 47 | 48 | function getLongest (array) { 49 | return array.reduce(function (longest, current) { 50 | if (current.length > longest) { 51 | return current.length; 52 | } 53 | return longest; 54 | }, 0); 55 | } 56 | 57 | function reportIssue (issue) { 58 | console.log(''); 59 | console.log( 60 | chalk[getColorForLevel(issue.level)].bold('• ' + issue.name), 61 | chalk.grey('(' + issue.level + ')') 62 | ); 63 | console.log( 64 | ' ', 65 | issue.context.replace('{{occurance}}', chalk.white.bold.bgRed(issue.occurance)) 66 | ); 67 | console.log(chalk.grey(wrap(chalk.green('✔ ') + issue.solution))); 68 | console.log(chalk.grey(wrap(chalk.red('✘ ') + issue.reason))); 69 | } 70 | 71 | function getColorForLevel (level) { 72 | if (level === 'error') { 73 | return 'red'; 74 | } 75 | if (level === 'warning') { 76 | return 'yellow'; 77 | } 78 | return 'cyan'; 79 | } 80 | 81 | function capitalizeFirstLetter (string) { 82 | return string.charAt(0).toUpperCase() + string.slice(1); 83 | } 84 | -------------------------------------------------------------------------------- /reporter/json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = report; 4 | 5 | function report (result, program) { 6 | var spacing = (program.pretty ? 4 : null); 7 | var output = JSON.stringify(result, null, spacing); 8 | console.log(output); 9 | } 10 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rowanmanning/joblint/3b3fbb4e4809831a3c6b507483ca9f52950c1175/screenshot.png -------------------------------------------------------------------------------- /test/browser/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Joblint Unit Tests 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 16 | 17 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/unit/lib/joblint.js: -------------------------------------------------------------------------------- 1 | // jshint maxstatements: false 2 | // jscs:disable disallowMultipleVarDecl, maximumLineLength 3 | 'use strict'; 4 | 5 | var assert = require('proclaim'); 6 | var mockery = require('mockery'); 7 | var sinon = require('sinon'); 8 | 9 | describe('lib/joblint', function () { 10 | var extend, joblint, options; 11 | 12 | beforeEach(function () { 13 | 14 | extend = sinon.spy(require('extend')); 15 | mockery.registerMock('extend', extend); 16 | 17 | joblint = require('../../../lib/joblint'); 18 | 19 | options = { 20 | rules: [] 21 | }; 22 | 23 | }); 24 | 25 | it('should be a function', function () { 26 | assert.isFunction(joblint); 27 | }); 28 | 29 | it('should have a `defaults` property', function () { 30 | assert.isObject(joblint.defaults); 31 | }); 32 | 33 | describe('.defaults', function () { 34 | var defaults; 35 | 36 | beforeEach(function () { 37 | defaults = joblint.defaults; 38 | }); 39 | 40 | it('should have a `rules` property', function () { 41 | assert.isObject(defaults.rules); 42 | assert.deepEqual(defaults.rules, require('../../../lib/rules')); 43 | }); 44 | 45 | }); 46 | 47 | describe('joblint()', function () { 48 | 49 | it('should default the options', function () { 50 | if (typeof window !== 'undefined') { 51 | return; 52 | } 53 | joblint('', options); 54 | assert.calledOnce(extend); 55 | assert.isObject(extend.firstCall.args[0]); 56 | assert.strictEqual(extend.firstCall.args[1], joblint.defaults); 57 | assert.strictEqual(extend.firstCall.args[2], options); 58 | }); 59 | 60 | it('should should return an object', function () { 61 | assert.isObject(joblint('', options)); 62 | }); 63 | 64 | }); 65 | 66 | describe('result', function () { 67 | var result; 68 | 69 | beforeEach(function () { 70 | result = joblint('', options); 71 | }); 72 | 73 | it('should should have a `counts` property', function () { 74 | assert.isObject(result.counts); 75 | }); 76 | 77 | it('should should have an `issues` property', function () { 78 | assert.isArray(result.issues); 79 | assert.lengthEquals(result.issues, 0); 80 | }); 81 | 82 | it('should be the same for each run', function () { 83 | options.rules.push({ 84 | triggers: [ 85 | 'he' 86 | ] 87 | }); 88 | var result1 = joblint('he should have his head screwed on', options); 89 | var result2 = joblint('he should have his head screwed on', options); 90 | assert.deepEqual(result1, result2); 91 | }); 92 | 93 | }); 94 | 95 | describe('rule matching', function () { 96 | 97 | it('should test the input for triggers in all rules', function () { 98 | options.rules.push({ 99 | triggers: [ 100 | 'he' 101 | ] 102 | }); 103 | options.rules.push({ 104 | triggers: [ 105 | 'his' 106 | ] 107 | }); 108 | var result = joblint('he should have his head screwed on', options); 109 | assert.lengthEquals(result.issues, 2); 110 | }); 111 | 112 | it('should test the input for all triggers', function () { 113 | options.rules.push({ 114 | triggers: [ 115 | 'he', 116 | 'his' 117 | ] 118 | }); 119 | var result = joblint('he should have his head screwed on', options); 120 | assert.lengthEquals(result.issues, 2); 121 | }); 122 | 123 | it('should find all matches for a trigger', function () { 124 | options.rules.push({ 125 | triggers: [ 126 | 'he|his' 127 | ] 128 | }); 129 | var result = joblint('he should have his head screwed on', options); 130 | assert.lengthEquals(result.issues, 2); 131 | }); 132 | 133 | it('should ignore case when matching triggers', function () { 134 | options.rules.push({ 135 | triggers: [ 136 | 'HE|HIS' 137 | ] 138 | }); 139 | var result = joblint('he should have his head screwed on', options); 140 | assert.lengthEquals(result.issues, 2); 141 | }); 142 | 143 | it('should not partial-match words outside of the trigger\'s bounds', function () { 144 | options.rules.push({ 145 | triggers: [ 146 | 'he' 147 | ] 148 | }); 149 | var result = joblint('she will have one hell of a time here', options); 150 | assert.lengthEquals(result.issues, 0); 151 | }); 152 | 153 | }); 154 | 155 | describe('result.issues', function () { 156 | 157 | describe('basics', function () { 158 | 159 | it('should include information about the rule that triggered the issue', function () { 160 | var rule = { 161 | name: 'foo', 162 | reason: 'bar', 163 | solution: 'baz', 164 | level: 'qux', 165 | increment: { 166 | foo: 1 167 | }, 168 | triggers: [ 169 | 'he' 170 | ] 171 | }; 172 | options.rules.push(rule); 173 | var result = joblint('he should have his head screwed on', options); 174 | assert.isObject(result.issues[0]); 175 | assert.strictEqual(result.issues[0].name, rule.name); 176 | assert.strictEqual(result.issues[0].reason, rule.reason); 177 | assert.strictEqual(result.issues[0].solution, rule.solution); 178 | assert.strictEqual(result.issues[0].level, rule.level); 179 | assert.deepEqual(result.issues[0].increment, rule.increment); 180 | assert.isUndefined(result.issues[0].triggers); 181 | }); 182 | 183 | it('should include the exact occurance of the trigger word', function () { 184 | options.rules.push({ 185 | triggers: [ 186 | 'he|his' 187 | ] 188 | }); 189 | var result = joblint('He should have HIS head screwed on if he wants this job', options); 190 | assert.strictEqual(result.issues[0].occurance, 'He'); 191 | assert.strictEqual(result.issues[1].occurance, 'HIS'); 192 | assert.strictEqual(result.issues[2].occurance, 'he'); 193 | }); 194 | 195 | it('should include the the position of the trigger word in the input text', function () { 196 | options.rules.push({ 197 | triggers: [ 198 | 'he|his' 199 | ] 200 | }); 201 | var result = joblint('he should have his head screwed on if he wants this job', options); 202 | assert.strictEqual(result.issues[0].position, 0); 203 | assert.strictEqual(result.issues[1].position, 15); 204 | assert.strictEqual(result.issues[2].position, 38); 205 | }); 206 | 207 | }); 208 | 209 | describe('context', function () { 210 | 211 | it('should include the context of the trigger word', function () { 212 | options.rules.push({ 213 | triggers: [ 214 | 'window' 215 | ] 216 | }); 217 | var result = joblint('How much is that doggie in the window? The one with the waggly tail. How much is that doggie in the window? I do hope that doggie\'s for sale.', options); 218 | assert.strictEqual(result.issues[0].context, '…that doggie in the {{occurance}}? The one with the…'); 219 | assert.strictEqual(result.issues[1].context, '…that doggie in the {{occurance}}? I do hope that doggie\'s…'); 220 | }); 221 | 222 | it('should not include line-breaks in the context', function () { 223 | options.rules.push({ 224 | triggers: [ 225 | 'much|window' 226 | ] 227 | }); 228 | var result = joblint('How much is that doggie in the window?\nThe one with the waggly tail.\nHow much is that doggie in the window?\nI do hope that doggie\'s for sale.', options); 229 | assert.strictEqual(result.issues[0].context, 'How {{occurance}} is that doggie in the window?'); 230 | assert.strictEqual(result.issues[1].context, '…that doggie in the {{occurance}}?'); 231 | assert.strictEqual(result.issues[2].context, 'How {{occurance}} is that doggie in the window?'); 232 | assert.strictEqual(result.issues[3].context, '…that doggie in the {{occurance}}?'); 233 | }); 234 | 235 | it('should add ellipses to the context if there are more words either side on the line', function () { 236 | options.rules.push({ 237 | triggers: [ 238 | 'trigger' 239 | ] 240 | }); 241 | var result = joblint('This is a longish line with trigger roughly in the middle so that we get ellipses.\nThis trigger is at the beginning of a longish line.\nThis is a longish line which has the trigger near the end.\nShort trigger line.', options); 242 | assert.strictEqual(result.issues[0].context, '…longish line with {{occurance}} roughly in the middle…'); 243 | assert.strictEqual(result.issues[1].context, 'This {{occurance}} is at the beginning of a longish…'); 244 | assert.strictEqual(result.issues[2].context, '…line which has the {{occurance}} near the end.'); 245 | assert.strictEqual(result.issues[3].context, 'Short {{occurance}} line.'); 246 | }); 247 | 248 | }); 249 | 250 | }); 251 | 252 | describe('result.counts', function () { 253 | 254 | it('should include incremented values for all triggered rules', function () { 255 | options.rules.push({ 256 | triggers: [ 257 | 'he' 258 | ], 259 | increment: { 260 | foo: 1 261 | } 262 | }); 263 | options.rules.push({ 264 | triggers: [ 265 | 'his' 266 | ], 267 | increment: { 268 | bar: 1 269 | } 270 | }); 271 | var result = joblint('he should have his head screwed on if he wants this job', options); 272 | assert.strictEqual(result.counts.foo, 2); 273 | assert.strictEqual(result.counts.bar, 1); 274 | }); 275 | 276 | it('should increment by the amount specified in the rule', function () { 277 | options.rules.push({ 278 | triggers: [ 279 | 'he' 280 | ], 281 | increment: { 282 | foo: 2 283 | } 284 | }); 285 | var result = joblint('he should have his head screwed on if he wants this job', options); 286 | assert.strictEqual(result.counts.foo, 4); 287 | }); 288 | 289 | it('should decrement when negative increments are found', function () { 290 | options.rules.push({ 291 | triggers: [ 292 | 'he' 293 | ], 294 | increment: { 295 | foo: 1 296 | } 297 | }); 298 | options.rules.push({ 299 | triggers: [ 300 | 'his' 301 | ], 302 | increment: { 303 | foo: -1 304 | } 305 | }); 306 | var result = joblint('he should have his head screwed on if he wants this job', options); 307 | assert.strictEqual(result.counts.foo, 1); 308 | }); 309 | 310 | it('should not decrement past 0 when negative increments are found', function () { 311 | options.rules.push({ 312 | triggers: [ 313 | 'he' 314 | ], 315 | increment: { 316 | foo: -1 317 | } 318 | }); 319 | options.rules.push({ 320 | triggers: [ 321 | 'his' 322 | ], 323 | increment: { 324 | foo: 1 325 | } 326 | }); 327 | var result = joblint('he should have his head screwed on if he wants this job', options); 328 | assert.strictEqual(result.counts.foo, 0); 329 | }); 330 | 331 | }); 332 | 333 | }); 334 | 335 | -------------------------------------------------------------------------------- /test/unit/setup.js: -------------------------------------------------------------------------------- 1 | // jshint maxstatements: false 2 | // jscs:disable disallowMultipleVarDecl, maximumLineLength 3 | 'use strict'; 4 | 5 | var assert = require('proclaim'); 6 | var mockery = require('mockery'); 7 | var sinon = require('sinon'); 8 | 9 | sinon.assert.expose(assert, { 10 | includeFail: false, 11 | prefix: '' 12 | }); 13 | 14 | beforeEach(function () { 15 | mockery.enable({ 16 | useCleanCache: true, 17 | warnOnUnregistered: false, 18 | warnOnReplace: false 19 | }); 20 | }); 21 | 22 | afterEach(function () { 23 | mockery.deregisterAll(); 24 | mockery.disable(); 25 | }); 26 | --------------------------------------------------------------------------------