@sap/eslint-plugin-cds 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +2 -1
  3. package/lib/api/index.js +9 -9
  4. package/lib/conf/all.js +20 -19
  5. package/lib/conf/index.js +10 -10
  6. package/lib/conf/recommended.js +17 -16
  7. package/lib/constants.js +15 -14
  8. package/lib/index.js +10 -11
  9. package/lib/parser.js +90 -82
  10. package/lib/rules/assoc2many-ambiguous-key.js +71 -70
  11. package/lib/rules/auth-no-empty-restrictions.js +16 -15
  12. package/lib/rules/auth-use-requires.js +19 -18
  13. package/lib/rules/auth-valid-restrict-grant.js +47 -46
  14. package/lib/rules/auth-valid-restrict-keys.js +19 -18
  15. package/lib/rules/auth-valid-restrict-to.js +61 -60
  16. package/lib/rules/auth-valid-restrict-where.js +44 -43
  17. package/lib/rules/extension-restrictions.js +69 -0
  18. package/lib/rules/index.js +23 -22
  19. package/lib/rules/latest-cds-version.js +21 -20
  20. package/lib/rules/min-node-version.js +22 -22
  21. package/lib/rules/no-db-keywords.js +17 -26
  22. package/lib/rules/no-dollar-prefixed-names.js +12 -11
  23. package/lib/rules/no-join-on-draft.js +27 -0
  24. package/lib/rules/require-2many-oncond.js +8 -8
  25. package/lib/rules/sql-cast-suggestion.js +13 -12
  26. package/lib/rules/start-elements-lowercase.js +42 -41
  27. package/lib/rules/start-entities-uppercase.js +26 -25
  28. package/lib/rules/valid-csv-header.js +58 -57
  29. package/lib/types.d.ts +1 -0
  30. package/lib/utils/Cache.js +17 -17
  31. package/lib/utils/Colors.js +8 -8
  32. package/lib/utils/createRule.js +166 -153
  33. package/lib/utils/findFuzzy.js +33 -38
  34. package/lib/utils/genDocs.js +224 -242
  35. package/lib/utils/getConfigPath.js +27 -27
  36. package/lib/utils/getConfiguredFileTypes.js +4 -4
  37. package/lib/utils/getFileExtensions.js +3 -3
  38. package/lib/utils/getProjectRootPath.js +25 -0
  39. package/lib/utils/isConfiguredFileType.js +11 -11
  40. package/lib/utils/rules.js +59 -59
  41. package/lib/utils/runRuleTester.js +76 -71
  42. package/package.json +7 -1
  43. package/lib/rules/no-join-on-draft-enabled-entities.js +0 -25
  44. package/lib/utils/createRuleDocs.js +0 -361
  45. package/lib/utils/jsonc.js +0 -1
  46. package/lib/utils/jsoncParser.js +0 -1
@@ -10,102 +10,103 @@
10
10
  * @returns {RuleModule}
11
11
  */
12
12
 
13
- const { SourceCode } = require("eslint");
14
- const path = require("path");
15
- const Cache = require("./Cache");
16
- const constants = require("../constants");
17
- const isConfiguredFileType = require("./isConfiguredFileType");
18
- const cds = require("@sap/cds");
19
- const LOG = cds.debug("lint:plugin");
20
- let filePrev = "";
13
+ const { SourceCode } = require('eslint')
14
+ const fs = require('fs')
15
+ const path = require('path')
16
+ const Cache = require('./Cache')
17
+ const constants = require('../constants')
18
+ const isConfiguredFileType = require('./isConfiguredFileType')
19
+ const getProjectRootPath = require('./getProjectRootPath')
20
+ const cds = require('@sap/cds')
21
+ const { exit } = require('process')
22
+ const LOG = cds.debug('lint:plugin')
23
+ let filePrev = ''
21
24
 
22
- const REGEX_COMMENT_START = "(/\\*|(.+)?//)(\\s?)+eslint-";
23
- const REGEX_COMMENTS = `${REGEX_COMMENT_START}(enable|disable)(-next)?(-line)?(.+)?`;
25
+ const REGEX_COMMENT_START = '(/\\*|(.+)?//)(\\s?)+eslint-'
26
+ const REGEX_COMMENTS = `${REGEX_COMMENT_START}(enable|disable)(-next)?(-line)?(.+)?`
24
27
 
25
28
  module.exports = (spec) => {
26
- let { meta, create } = spec;
27
- meta = setMetaDefaults(meta);
29
+ let { meta, create } = spec
30
+ meta = setMetaDefaults(meta)
28
31
 
29
32
  return {
30
33
  meta,
31
34
  create: (context) => ({
32
35
  Program: (node) => {
33
36
  try {
34
- const file = context.getFilename();
37
+ const file = context.getFilename()
35
38
  if (file !== filePrev) {
36
- LOG && LOG(`File: ${context.getFilename()}`);
39
+ LOG && LOG(`File: ${context.getFilename()}`)
37
40
  }
38
- const cdscontext = extendContext(node, context, meta);
39
- const { isTest, isValidFile, doEnvironmentChecks, doRootModelChecks } = checkEntryCriteria(meta, cdscontext);
40
-
41
+ const cdscontext = extendContext(node, context, meta)
42
+ Cache.set('context', cdscontext)
43
+ const { isTest, isValidFile, doEnvironmentChecks, doRootModelChecks } = checkEntryCriteria(meta, cdscontext)
41
44
  switch (meta.model) {
42
- case "none":
45
+ case 'none':
43
46
  if (doEnvironmentChecks) {
44
47
  if (isTest || !Cache.has(`rule:${cdscontext.id}`)) {
45
- LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`);
46
- Cache.set(`rule:${cdscontext.id}`, "done");
47
- createReport(node, cdscontext, meta, create);
48
+ LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
49
+ Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
50
+ createReport(node, cdscontext, meta, create)
48
51
  }
49
52
  }
50
- break;
53
+ break
51
54
 
52
- case "inferred":
55
+ case 'inferred':
53
56
  if (isValidFile && doRootModelChecks) {
54
- if (isTest || !Cache.has(`rule:${context.id}`)) {
55
- LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`);
56
- Cache.set(`rule:${cdscontext.id}`, "done");
57
- createReport(node, cdscontext, meta, create);
57
+ if (isTest || !Cache.has(`rule:${cdscontext.id}:${Cache.get('rootpath')}`)) {
58
+ LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
59
+ Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
60
+ createReport(node, cdscontext, meta, create)
58
61
  } else {
59
62
  if (Cache.has(`report:${context.getFilename()}:${context.id}`)) {
60
- const reports = Cache.get(`report:${context.getFilename()}:${context.id}`);
63
+ const reports = Cache.get(`report:${context.getFilename()}:${context.id}`)
61
64
  for (const r of Array.from(reports)) {
62
- context.report(r);
65
+ context.report(JSON.parse(r))
63
66
  }
64
- Cache.remove(`report:${context.getFilename()}:${context.id}`);
67
+ Cache.remove(`report:${context.getFilename()}:${context.id}`)
68
+ Cache.set(`rule:${cdscontext.id}:${Cache.get('rootpath')}`, 'done')
65
69
  }
66
70
  }
67
71
  }
68
- break;
72
+ break
69
73
 
70
74
  default:
71
75
  if (isValidFile) {
72
- LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`);
73
- createReport(node, cdscontext, meta, create);
76
+ LOG && LOG(` Model: "${meta.model}" Rule: ${context.id}`)
77
+ createReport(node, cdscontext, meta, create)
74
78
  }
75
- break;
79
+ break
76
80
  }
77
- filePrev = file;
81
+ filePrev = file
78
82
  } catch (err) {
79
- LOG && LOG(err);
83
+ console.error(err)
80
84
  }
81
85
  }
82
86
  })
83
- };
84
- };
87
+ }
88
+ }
85
89
 
86
- function isRunningWithCDSLint() {
87
- return process.argv[0].endsWith("node") && process.argv[1].endsWith("cds") && process.argv[2] === "lint";
90
+ function isRunningWithCDSLint () {
91
+ return process.argv[0].endsWith('node') && process.argv[1].endsWith('cds') && process.argv[2] === 'lint'
88
92
  }
89
93
 
90
- function checkEntryCriteria(meta, cdscontext) {
91
- const isTest = Cache.has("test");
92
- cds.resolve.cache = {};
93
- const roots = cds.resolve("*", { root: process.cwd() });
94
- const isProjectRoot = roots && roots.length > 0 ? true : false;
95
- const isValidFile = isConfiguredFileType(cdscontext.getFilename(), "FILES");
96
- const isRunningWithCdsLint = isRunningWithCDSLint();
97
- const doRootModelChecks = isTest || (isProjectRoot && (isRunningWithCdsLint || cdscontext.options.includes("show")));
94
+ function checkEntryCriteria (meta, cdscontext) {
95
+ const isTest = Cache.has('test')
96
+ const hasProjectRoots = Cache.has(`roots:${Cache.get('rootpath')}`)
97
+ const isValidFile = isConfiguredFileType(cdscontext.getFilename(), 'FILES')
98
+ const doRootModelChecks = isTest || (hasProjectRoots && (isRunningWithCDSLint() || cdscontext.options.includes('show')))
98
99
  // Also lint empty folders (i.e. lintText "" API)
99
100
  const doEnvironmentChecks =
100
- isTest || (isProjectRoot && isRunningWithCdsLint && cdscontext.getSourceCode().lines[0] === "");
101
- return { isTest, isValidFile, doRootModelChecks, doEnvironmentChecks };
101
+ isTest || (hasProjectRoots && isRunningWithCDSLint() && cdscontext.getSourceCode().lines[0] === '')
102
+ return { isTest, isValidFile, doRootModelChecks, doEnvironmentChecks }
102
103
  }
103
104
 
104
- function setMetaDefaults(meta) {
105
- if (!meta.severity) meta.severity = constants.DEFAULT_RULE_SEVERITY;
106
- if (meta.docs && !meta.docs.category) meta.docs.category = constants.DEFAULT_RULE_CATEGORY;
107
- if (!meta.model) meta.model = "parsed";
108
- return meta;
105
+ function setMetaDefaults (meta) {
106
+ if (!meta.severity) meta.severity = constants.DEFAULT_RULE_SEVERITY
107
+ if (meta.docs && !meta.docs.category) meta.docs.category = constants.DEFAULT_RULE_CATEGORY
108
+ if (!meta.model) meta.model = 'parsed'
109
+ return meta
109
110
  }
110
111
 
111
112
  /**
@@ -119,9 +120,8 @@ function setMetaDefaults(meta) {
119
120
  * @param {*} create
120
121
  * @returns
121
122
  */
122
- function createReport(node, cdscontext, meta, create) {
123
- const handlers = create(cdscontext);
124
-
123
+ function createReport (node, cdscontext, meta, create) {
124
+ const handlers = create(cdscontext)
125
125
  /**
126
126
  * TODO: Can these be rewritten to have a visitor? Note, that so far,
127
127
  * rules without a visitor cannot use eslint disable comments
@@ -130,129 +130,142 @@ function createReport(node, cdscontext, meta, create) {
130
130
  * - Environment rules
131
131
  */
132
132
  switch (typeof handlers) {
133
- case "function":
134
- handlers();
135
- break;
133
+ case 'function':
134
+ handlers()
135
+ break
136
+
137
+ case 'object': {
138
+ if (meta.model !== 'none') {
139
+ const model = cdscontext.getModel()
136
140
 
137
- case "object": {
138
- if (meta.model !== "none") {
139
- const model = cdscontext.getModel();
140
141
  if (model) {
141
142
  model.forall((d) => {
142
143
  Object.entries(handlers)
143
144
  .filter(([type, lazy]) => d.is(type))
144
145
  .forEach(([lazy, handler]) => {
145
146
  try {
146
- handler(d);
147
+ handler(d)
147
148
  } catch (err) {
148
- console.log(`Error in rule "${cdscontext.id}" at ${d.name}`, err);
149
+ console.log(`Error in rule "${cdscontext.id}" at ${d.name}`, err)
149
150
  }
150
- });
151
- });
151
+ })
152
+ })
152
153
  }
153
154
  }
154
- break;
155
+ break
155
156
  }
156
157
  }
157
158
  }
158
159
 
159
- function extendContext(node, context, meta) {
160
- if (!Cache.has("test") && !Cache.has("rootpath")) {
161
- Cache.set("rootpath", path.resolve("."));
160
+ function extendContext (node, context, meta) {
161
+ if (!Cache.has('test')) {
162
+ const filePath = context.getFilename()
163
+ const rootPath = filePath && fs.existsSync(filePath) ? getProjectRootPath(filePath) : ''
164
+ if (rootPath) {
165
+ Cache.set('rootpath', rootPath)
166
+ }
162
167
  }
163
168
 
164
169
  const reportWrapper = (r) => {
165
- const line = r.loc ? r.loc.start.line : r.node.loc.start.line;
170
+ const line = r.loc ? r.loc.start.line : r.node.loc.start.line
166
171
  if (!isRuleDisabled(line, context)) {
167
-
168
- if (meta.model === "inferred") {
169
- const file = resolveFilePath(r.file);
172
+ if (meta.model === 'inferred') {
173
+ if (!r.file) {
174
+ console.error(`Rule ${context.id} must return a "file" property in the rule report!`)
175
+ exit(1)
176
+ }
177
+ const file = Cache.get('rootpath') ? resolveFilePath(r.file) : r.file
170
178
  if (cdscontext.getFilename() === file) {
171
- delete r.file;
172
- context.report(r);
173
- } else {
174
- delete r.file;
175
- if (r.messageId) {
176
- r.message = meta.messages[r.messageId];
177
- delete r.message;
178
- }
179
- if (r) {
180
- let reports = new Set();
181
- if (Cache.has(`report:${file}:${context.id}`)) {
182
- reports = Cache.get(`report:${file}:${context.id}`);
183
- }
184
- reports.add(r);
185
- Cache.set(`report:${file}:${context.id}`, reports);
186
- }
179
+ delete r.file
180
+ context.report(r)
181
+ }
182
+ if (r.file) {
183
+ cacheReport(r, file, context, meta)
187
184
  }
188
-
189
185
  } else {
190
- context.report(r);
186
+ context.report(r)
191
187
  }
192
188
  }
193
- };
189
+ }
194
190
 
195
- const descriptors = Object.getOwnPropertyDescriptors(context);
191
+ const descriptors = Object.getOwnPropertyDescriptors(context)
196
192
  descriptors.report = {
197
193
  value: reportWrapper,
198
194
  writable: false,
199
195
  enumerable: true,
200
196
  configurable: false
201
- };
197
+ }
202
198
 
203
- const cdscontext = Object.create(Object.getPrototypeOf(context), descriptors);
199
+ const cdscontext = Object.create(Object.getPrototypeOf(context), descriptors)
204
200
  cdscontext.getModel =
205
- meta.model === "inferred" ? context.parserServices.getInferredCsn : context.parserServices.getParsedCsn;
201
+ meta.model === 'inferred' ? context.parserServices.getInferredCsn : context.parserServices.getParsedCsn
206
202
  cdscontext.getEnvironment = () => {
207
- const options = context.options;
208
- return options && options[0] && options[0].environment ? options[0].environment : undefined;
209
- };
210
- cdscontext.getLocation = context.parserServices.getLocation;
211
- cdscontext.getNode = Object.keys(context.parserServices).length > 0 ? context.parserServices.getNode : () => node;
212
- return cdscontext;
203
+ const options = context.options
204
+ return options && options[0] && options[0].environment ? options[0].environment : undefined
205
+ }
206
+ cdscontext.getLocation = context.parserServices.getLocation
207
+ cdscontext.getNode = Object.keys(context.parserServices).length > 0 ? context.parserServices.getNode : () => node
208
+ return cdscontext
213
209
  }
214
210
 
215
- function isRuleDisabled(line, cdscontext) {
216
- let isDisabled = false;
211
+ function isRuleDisabled (line, cdscontext) {
212
+ let isDisabled = false
217
213
  if (cdscontext) {
218
- const sourcecode = cdscontext.getSourceCode();
219
- const rulesDisabled = getDisabled(sourcecode.getText(), sourcecode, line);
220
- const id = cdscontext.id;
221
- isDisabled = line && id in rulesDisabled && rulesDisabled[id] === "off";
214
+ const sourcecode = cdscontext.getSourceCode()
215
+ const rulesDisabled = getDisabled(sourcecode.getText(), sourcecode, line)
216
+ const id = cdscontext.id
217
+ isDisabled = line && id in rulesDisabled && rulesDisabled[id] === 'off'
222
218
  }
223
- return isDisabled;
219
+ return isDisabled
224
220
  }
225
221
 
226
- function getDisabled(code, sourcecode, line) {
227
- const listDisabled = [];
228
- let rules = Cache.get("rules");
229
- const rulesDisabled = Object.keys(rules).reduce((o, key) => ({ ...o, [key]: "on" }), {});
230
- let matches = [];
222
+ function cacheReport (r, file, context, meta) {
223
+ delete r.file
224
+ r.node.range = []
225
+ if (r.messageId) {
226
+ r.message = meta.messages[r.messageId]
227
+ delete r.message
228
+ }
229
+ if (r) {
230
+ let reports = new Set()
231
+ if (Cache.has(`report:${file}:${context.id}`)) {
232
+ reports = Cache.get(`report:${file}:${context.id}`)
233
+ }
234
+ reports.add(JSON.stringify(r))
235
+ Cache.set(`report:${file}:${context.id}`, reports)
236
+ }
237
+ }
238
+
239
+ function getDisabled (code, sourcecode, line) {
240
+ const listDisabled = []
241
+ const rules = Cache.get('rules')
242
+ const rulesDisabled = Object.keys(rules).reduce((o, key) => ({ ...o, [key]: 'on' }), {})
243
+ let matches = []
231
244
  if (code) {
232
- matches = [...code.matchAll(REGEX_COMMENTS)];
245
+ matches = [...code.matchAll(REGEX_COMMENTS)]
233
246
  if (matches.length > 0) {
234
247
  matches.forEach((match) => {
235
248
  if (match) {
236
- const index = match.index;
237
- match = match[0];
238
- if (match.includes("*/")) {
239
- match = match.split("*/")[0].replace("/*", "");
240
- } else if (match.includes("//")) {
241
- match = match.split("//")[1];
249
+ const index = match.index
250
+ match = match[0]
251
+ if (match.includes('*/')) {
252
+ match = match.split('*/')[0].replace('/*', '')
253
+ } else if (match.includes('//')) {
254
+ match = match.split('//')[1]
242
255
  }
243
256
  if (match) {
244
- match = match.trim();
257
+ match = match.trim()
245
258
  }
246
- ["disable", "enable"].forEach((keyword) => {
247
- const loc = sourcecode.getLocFromIndex(index);
248
- const disableType = match.split(" ")[0];
249
- let disableRules = match.split(`${disableType} `)[1];
259
+ ['disable', 'enable'].forEach((keyword) => {
260
+ const loc = sourcecode.getLocFromIndex(index)
261
+ const disableType = match.split(' ')[0]
262
+ let disableRules = match.split(`${disableType} `)[1]
250
263
  disableRules = disableRules
251
- ? disableRules.split(",").map((rule) => rule.trim())
252
- : Object.keys(rules).map((rule) => `@sap/cds/${rule}`);
253
- let comment = {};
264
+ ? disableRules.split(',').map((rule) => rule.trim())
265
+ : Object.keys(rules).map((rule) => `@sap/cds/${rule}`)
266
+ let comment = {}
254
267
  if ([`eslint-${keyword}`, `eslint-${keyword}-line`, `eslint-${keyword}-next-line`].includes(disableType)) {
255
- comment = disableType.includes("-next-line")
268
+ comment = disableType.includes('-next-line')
256
269
  ? {
257
270
  lineComment: loc.line,
258
271
  lineDisabled: loc.line + 1,
@@ -264,41 +277,41 @@ function getDisabled(code, sourcecode, line) {
264
277
  lineDisabled: loc.line,
265
278
  rules: disableRules,
266
279
  type: keyword
267
- };
268
- if (!disableType.includes("-line")) {
269
- comment.lineDisabled = "EOF";
280
+ }
281
+ if (!disableType.includes('-line')) {
282
+ comment.lineDisabled = 'EOF'
270
283
  }
271
284
  }
272
- listDisabled.push(comment);
273
- });
285
+ listDisabled.push(comment)
286
+ })
274
287
  }
275
- });
288
+ })
276
289
  for (const el of listDisabled.filter(
277
- (d) => d.lineComment > line && (d.lineDisabled === "EOF" || d.lineDisabled === line)
290
+ (d) => d.lineComment > line && (d.lineDisabled === 'EOF' || d.lineDisabled === line)
278
291
  )) {
279
- if (el.lineDisabled === "EOF") {
280
- el.lineDisabled = getLastLine(code);
292
+ if (el.lineDisabled === 'EOF') {
293
+ el.lineDisabled = getLastLine(code)
281
294
  }
282
295
  if (el.rules) {
283
296
  el.rules.forEach((rule) => {
284
- if (el.type === "disable") {
285
- rulesDisabled[rule] = "off";
286
- } else if (el.type === "enable") {
287
- rulesDisabled[rule] = "on";
297
+ if (el.type === 'disable') {
298
+ rulesDisabled[rule] = 'off'
299
+ } else if (el.type === 'enable') {
300
+ rulesDisabled[rule] = 'on'
288
301
  }
289
- });
302
+ })
290
303
  }
291
304
  }
292
305
  }
293
306
  }
294
- return rulesDisabled;
307
+ return rulesDisabled
295
308
  }
296
309
 
297
- function getLastLine(code) {
298
- const lines = typeof code === "string" ? SourceCode.splitLines(code) : code;
299
- return lines.length - 1;
310
+ function getLastLine (code) {
311
+ const lines = typeof code === 'string' ? SourceCode.splitLines(code) : code
312
+ return lines.length - 1
300
313
  }
301
314
 
302
- function resolveFilePath(file) {
303
- return path.isAbsolute(file) ? file : path.join(Cache.get("rootpath"), file);
315
+ function resolveFilePath (file) {
316
+ return path.isAbsolute(file) ? file : path.join(Cache.get('rootpath'), file)
304
317
  }
@@ -7,86 +7,81 @@
7
7
  * @returns array with best matches, is never null but might be empty in case no search was possible
8
8
  */
9
9
 
10
+ const cache = {}
10
11
 
11
- const cache = {};
12
-
13
-
14
- module.exports = (input, list, log, keepCase=false) => {
15
- let minDistWords = [];
12
+ module.exports = (input, list, log, keepCase = false) => {
13
+ let minDistWords = []
16
14
 
17
15
  if (input.length > 50 || list.length > 50) {
18
- return minDistWords;
16
+ return minDistWords
19
17
  }
20
18
 
21
- let minDist = Number.MAX_SAFE_INTEGER;
19
+ let minDist = Number.MAX_SAFE_INTEGER
22
20
 
23
- log && log('\nword\t\tlevDist\t\ttime(ms)');
21
+ log && log('\nword\t\tlevDist\t\ttime(ms)')
24
22
 
25
- let runtime = 0;
23
+ let runtime = 0
26
24
 
27
25
  for (const word of list) {
28
-
29
-
30
- const start = log && Date.now();
31
- let levDist;
26
+ const start = log && Date.now()
27
+ let levDist
32
28
  if (word === word.toUpperCase() && !keepCase) {
33
- levDist = levDistance(input.toUpperCase(), word);
29
+ levDist = levDistance(input.toUpperCase(), word)
34
30
  } else {
35
- levDist = levDistance(input, word);
31
+ levDist = levDistance(input, word)
36
32
  }
37
33
 
38
34
  if (log) {
39
- const duration = Date.now() - start;
40
- runtime = runtime + duration;
41
- log(`${word}\t\t${levDist}\t\t${duration}`);
35
+ const duration = Date.now() - start
36
+ runtime = runtime + duration
37
+ log(`${word}\t\t${levDist}\t\t${duration}`)
42
38
  }
43
39
 
44
40
  if (levDist === minDist) {
45
- minDistWords.push(word);
41
+ minDistWords.push(word)
46
42
  }
47
43
 
48
44
  if (levDist < minDist) {
49
- minDist = levDist;
50
- minDistWords = [word];
45
+ minDist = levDist
46
+ minDistWords = [word]
51
47
  }
52
48
  }
53
49
 
54
- log && log(`runtime: ${runtime}ms`);
50
+ log && log(`runtime: ${runtime}ms`)
55
51
 
56
- return minDistWords.sort();
52
+ return minDistWords.sort()
57
53
  }
58
54
 
59
55
  const levDistance = (a, b) => {
60
-
61
56
  if (cache[a] && cache[a][b]) {
62
- return cache[a][b];
57
+ return cache[a][b]
63
58
  }
64
59
 
65
60
  if (a.length === 0) {
66
- return addToCache(a, b, b.length);
61
+ return addToCache(a, b, b.length)
67
62
  }
68
63
 
69
64
  if (b.length === 0) {
70
- return addToCache(a, b, a.length);
65
+ return addToCache(a, b, a.length)
71
66
  }
72
67
 
73
- const tail_a = a.substring(1);
74
- const tail_b = b.substring(1);
68
+ const tailA = a.substring(1)
69
+ const tailB = b.substring(1)
75
70
 
76
71
  if (a[0] === b[0]) {
77
- return levDistance(tail_a, tail_b);
72
+ return levDistance(tailA, tailB)
78
73
  }
79
74
 
80
- const lev1 = levDistance(tail_a, b);
81
- const lev2 = levDistance(a, tail_b);
82
- const lev3 = levDistance(tail_a, tail_b);
75
+ const lev1 = levDistance(tailA, b)
76
+ const lev2 = levDistance(a, tailB)
77
+ const lev3 = levDistance(tailA, tailB)
83
78
 
84
- const levDist = Math.min(lev1, lev2, lev3) + 1;
85
- return addToCache(a, b, levDist);
79
+ const levDist = Math.min(lev1, lev2, lev3) + 1
80
+ return addToCache(a, b, levDist)
86
81
  }
87
82
 
88
83
  const addToCache = (a, b, value) => {
89
- cache[a] = cache[a] || {};
90
- cache[a][b] = value;
91
- return value;
84
+ cache[a] = cache[a] || {}
85
+ cache[a][b] = value
86
+ return value
92
87
  }