UNPKG

5.39 kBJavaScriptView Raw
1'use strict';
2const path = require('path');
3const {camelCase, kebabCase, snakeCase, upperFirst} = require('lodash');
4const getDocumentationUrl = require('./utils/get-documentation-url');
5const cartesianProductSamples = require('./utils/cartesian-product-samples');
6
7const pascalCase = string => upperFirst(camelCase(string));
8const numberRegex = /\d+/;
9const PLACEHOLDER = '\uFFFF\uFFFF\uFFFF';
10const PLACEHOLDER_REGEX = new RegExp(PLACEHOLDER, 'i');
11const isIgnoredChar = char => !/^[a-z\d-_$]$/i.test(char);
12
13function ignoreNumbers(fn) {
14 return string => {
15 const stack = [];
16 let execResult = numberRegex.exec(string);
17
18 while (execResult) {
19 stack.push(execResult[0]);
20 string = string.replace(execResult[0], PLACEHOLDER);
21 execResult = numberRegex.exec(string);
22 }
23
24 let withCase = fn(string);
25
26 while (stack.length > 0) {
27 withCase = withCase.replace(PLACEHOLDER_REGEX, stack.shift());
28 }
29
30 return withCase;
31 };
32}
33
34const cases = {
35 camelCase: {
36 fn: camelCase,
37 name: 'camel case'
38 },
39 kebabCase: {
40 fn: kebabCase,
41 name: 'kebab case'
42 },
43 snakeCase: {
44 fn: snakeCase,
45 name: 'snake case'
46 },
47 pascalCase: {
48 fn: pascalCase,
49 name: 'pascal case'
50 }
51};
52
53/**
54Get the cases specified by the option.
55
56@param {object} options
57@returns {string[]} The chosen cases.
58*/
59function getChosenCases(options) {
60 if (options.case) {
61 return [options.case];
62 }
63
64 if (options.cases) {
65 const cases = Object.keys(options.cases)
66 .filter(cases => options.cases[cases]);
67
68 return cases.length > 0 ? cases : ['kebabCase'];
69 }
70
71 return ['kebabCase'];
72}
73
74function validateFilename(words, caseFunctions) {
75 return words
76 .filter(({ignored}) => !ignored)
77 .every(({word}) => caseFunctions.some(fn => fn(word) === word));
78}
79
80function fixFilename(words, caseFunctions, {leading, extension}) {
81 const replacements = words
82 .map(({word, ignored}) => ignored ? [word] : caseFunctions.map(fn => fn(word)));
83
84 const {
85 samples: combinations
86 } = cartesianProductSamples(replacements);
87
88 return combinations.map(parts => `${leading}${parts.join('')}${extension}`);
89}
90
91const leadingUnderscoresRegex = /^(?<leading>_+)(?<tailing>.*)$/;
92function splitFilename(filename) {
93 const result = leadingUnderscoresRegex.exec(filename) || {groups: {}};
94 const {leading = '', tailing = filename} = result.groups;
95
96 const words = [];
97
98 let lastWord;
99 for (const char of tailing) {
100 const isIgnored = isIgnoredChar(char);
101
102 if (lastWord && lastWord.ignored === isIgnored) {
103 lastWord.word += char;
104 } else {
105 lastWord = {
106 word: char,
107 ignored: isIgnored
108 };
109 words.push(lastWord);
110 }
111 }
112
113 return {
114 leading,
115 words
116 };
117}
118
119/**
120Turns `[a, b, c]` into `a, b, or c`.
121
122@param {string[]} words
123@returns {string}
124*/
125function englishishJoinWords(words) {
126 if (words.length === 1) {
127 return words[0];
128 }
129
130 if (words.length === 2) {
131 return `${words[0]} or ${words[1]}`;
132 }
133
134 words = words.slice();
135 const last = words.pop();
136 return `${words.join(', ')}, or ${last}`;
137}
138
139const create = context => {
140 const options = context.options[0] || {};
141 const chosenCases = getChosenCases(options);
142 const ignore = (options.ignore || []).map(item => {
143 if (item instanceof RegExp) {
144 return item;
145 }
146
147 return new RegExp(item, 'u');
148 });
149 const chosenCasesFunctions = chosenCases.map(case_ => ignoreNumbers(cases[case_].fn));
150 const filenameWithExtension = context.getFilename();
151
152 if (filenameWithExtension === '<input>' || filenameWithExtension === '<text>') {
153 return {};
154 }
155
156 return {
157 Program: node => {
158 const extension = path.extname(filenameWithExtension);
159 const filename = path.basename(filenameWithExtension, extension);
160 const base = filename + extension;
161
162 if (base === 'index.js' || ignore.some(regexp => regexp.test(base))) {
163 return;
164 }
165
166 const {leading, words} = splitFilename(filename);
167 const isValid = validateFilename(words, chosenCasesFunctions);
168
169 if (isValid) {
170 return;
171 }
172
173 const renamedFilenames = fixFilename(words, chosenCasesFunctions, {
174 leading,
175 extension
176 });
177
178 context.report({
179 node,
180 messageId: chosenCases.length > 1 ? 'renameToCases' : 'renameToCase',
181 data: {
182 chosenCases: englishishJoinWords(chosenCases.map(x => cases[x].name)),
183 renamedFilenames: englishishJoinWords(renamedFilenames.map(x => `\`${x}\``))
184 }
185 });
186 }
187 };
188};
189
190const schema = [
191 {
192 oneOf: [
193 {
194 properties: {
195 case: {
196 enum: [
197 'camelCase',
198 'snakeCase',
199 'kebabCase',
200 'pascalCase'
201 ]
202 },
203 ignore: {
204 type: 'array',
205 uniqueItems: true
206 }
207 },
208 additionalProperties: false
209 },
210 {
211 properties: {
212 cases: {
213 properties: {
214 camelCase: {
215 type: 'boolean'
216 },
217 snakeCase: {
218 type: 'boolean'
219 },
220 kebabCase: {
221 type: 'boolean'
222 },
223 pascalCase: {
224 type: 'boolean'
225 }
226 },
227 additionalProperties: false
228 },
229 ignore: {
230 type: 'array',
231 uniqueItems: true
232 }
233 },
234 additionalProperties: false
235 }
236 ]
237 }
238];
239
240module.exports = {
241 create,
242 meta: {
243 type: 'suggestion',
244 docs: {
245 url: getDocumentationUrl(__filename)
246 },
247 schema,
248 messages: {
249 renameToCase: 'Filename is not in {{chosenCases}}. Rename it to {{renamedFilenames}}.',
250 renameToCases: 'Filename is not in {{chosenCases}}. Rename it to {{renamedFilenames}}.'
251 }
252 }
253};