UNPKG

5.66 kBJavaScriptView Raw
1'use strict';
2
3const { readFileSync } = require('fs');
4const { resolve } = require('path');
5const { getCustomRuleConfig } = require('../utils/custom-rule-config');
6const { camelize, pascalize } = require('humps');
7
8const DEFAULT_MESSAGE =
9 "The usage of the non-inclusive word '{{word}}' is discouraged, use '{{suggestion}}' instead.";
10const PROJECT_URL =
11 'http://github.com/muenzpraeger/eslint-plugin-inclusive-language/tree/primary/docs/rules/use-inclusive-words.md';
12const RULE_CATEGORY = 'Language';
13const RULE_DESCRIPTION = 'highlights where non-inclusive language is used';
14const SUGGESTION_MESSAGE = "Replace word '{{word}}' with '{{suggestion}}.'";
15
16let ruleConfig = JSON.parse(
17 readFileSync(resolve(__dirname, '../config/inclusive-words.json')),
18 'utf8'
19);
20
21let customConfig;
22
23/**
24 * Remove allowed terms from the list of words, checking
25 * for partial or complete matches based on each allowed
26 * term's configuration.
27 *
28 * @param {string[]} words The list of words to filter
29 * @returns {string[]} The list of words excluding any allowed terms
30 */
31function excludeAllowedTerms(words) {
32 return words.filter((word) => {
33 let isAllowedTerm;
34
35 if (customConfig) {
36 isAllowedTerm = customConfig.allowedTerms.find(
37 ({ term, allowPartialMatches }) => {
38 if (!allowPartialMatches) {
39 return word.toLowerCase() === term;
40 }
41 return word.toLowerCase().includes(term);
42 }
43 );
44 }
45
46 return !isAllowedTerm;
47 });
48}
49
50function buildFixer(context, node, regex, word, newValue) {
51 return (fixer) => {
52 const source = context.getSourceCode().getText(node);
53 const result = replace(source, regex, word, newValue);
54 return fixer.replaceText(node, result);
55 };
56}
57
58function replace(source, regex, word, replacement) {
59 return source.replace(regex, (fullMatch, capture) => {
60 const matcher = new RegExp(word, 'i');
61 if (capture.toUpperCase() === capture)
62 return fullMatch.replace(matcher, replacement.toUpperCase());
63 if (capture.toLowerCase() === capture)
64 return fullMatch.replace(matcher, replacement.toLowerCase());
65 if (capture[0].toLowerCase() == capture[0])
66 return fullMatch.replace(matcher, camelize(replacement));
67 return fullMatch.replace(matcher, pascalize(replacement));
68 });
69}
70
71function validateIfInclusive(context, node, value) {
72 if (typeof value !== 'string') return;
73
74 let regex;
75 let result;
76
77 if (customConfig) {
78 result = customConfig.words.find((wordDeclaration) => {
79 // match whole words and partial words, at the end and beginning of sentences
80 regex = new RegExp(
81 `[\\w-_/]*(${wordDeclaration.word})[\\w-_/]*`,
82 'ig'
83 );
84 const matches = value.match(regex);
85 if (matches) return excludeAllowedTerms(matches).length;
86 });
87 }
88
89 if (!result) {
90 result = ruleConfig.words.find((wordDeclaration) => {
91 // match whole words and partial words, at the end and beginning of sentences
92 regex = new RegExp(
93 `[\\w-_/]*(${wordDeclaration.word})[\\w-_/]*`,
94 'ig'
95 );
96 const matches = value.match(regex);
97 if (matches) return excludeAllowedTerms(matches).length;
98 });
99 }
100
101 if (!result) return;
102
103 const { word, explanation } = result;
104 // backwards-compatibility with singular suggestions
105 const suggestions = result.suggestions
106 ? result.suggestions
107 : [result.suggestion];
108
109 const suggest = suggestions.map((suggestion) => {
110 return {
111 desc: SUGGESTION_MESSAGE,
112 data: {
113 word,
114 suggestion
115 },
116 fix: buildFixer(context, node, regex, word, suggestion)
117 };
118 });
119
120 const reportObject = {
121 data: {
122 word,
123 suggestion: suggestions[0]
124 },
125 node,
126 suggest,
127 fix:
128 customConfig && customConfig.autofix
129 ? buildFixer(context, node, regex, word, suggestions[0])
130 : undefined,
131 message: explanation || DEFAULT_MESSAGE
132 };
133
134 context.report(reportObject);
135}
136
137module.exports = {
138 meta: {
139 docs: {
140 description: RULE_DESCRIPTION,
141 category: RULE_CATEGORY,
142 recommended: true,
143 url: PROJECT_URL
144 },
145 fixable: 'code',
146 schema: [
147 {
148 customFile: 'string'
149 }
150 ],
151 type: 'suggestion'
152 },
153 create: function (context) {
154 customConfig = getCustomRuleConfig(context);
155
156 return {
157 Literal(node) {
158 customConfig && customConfig.lintStrings
159 ? validateIfInclusive(context, node, node.value)
160 : undefined;
161 },
162 Identifier(node) {
163 validateIfInclusive(context, node, node.name);
164 },
165 JSXIdentifier(node) {
166 validateIfInclusive(context, node, node.name);
167 },
168 TemplateElement(node) {
169 validateIfInclusive(context, node, node.value.raw);
170 },
171 Program(node) {
172 node.comments
173 .filter((c) => c.type !== 'Shebang')
174 .forEach((c) => {
175 validateIfInclusive(context, c, c.value);
176 });
177 }
178 };
179 }
180};