1 | 'use strict';
|
2 |
|
3 | const { readFileSync } = require('fs');
|
4 | const { resolve } = require('path');
|
5 | const { getCustomRuleConfig } = require('../utils/custom-rule-config');
|
6 | const { camelize, pascalize } = require('humps');
|
7 |
|
8 | const DEFAULT_MESSAGE =
|
9 | "The usage of the non-inclusive word '{{word}}' is discouraged, use '{{suggestion}}' instead.";
|
10 | const PROJECT_URL =
|
11 | 'http://github.com/muenzpraeger/eslint-plugin-inclusive-language/tree/primary/docs/rules/use-inclusive-words.md';
|
12 | const RULE_CATEGORY = 'Language';
|
13 | const RULE_DESCRIPTION = 'highlights where non-inclusive language is used';
|
14 | const SUGGESTION_MESSAGE = "Replace word '{{word}}' with '{{suggestion}}.'";
|
15 |
|
16 | let ruleConfig = JSON.parse(
|
17 | readFileSync(resolve(__dirname, '../config/inclusive-words.json')),
|
18 | 'utf8'
|
19 | );
|
20 |
|
21 | let customConfig;
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | function 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 |
|
50 | function 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 |
|
58 | function 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 |
|
71 | function 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 |
|
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 |
|
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 |
|
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 |
|
137 | module.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 | };
|