├── .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 |
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 |
--------------------------------------------------------------------------------