@sap/eslint-plugin-cds 2.5.0 → 2.6.1

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 +24 -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 +16 -14
  8. package/lib/index.js +17 -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 +49 -46
  14. package/lib/rules/auth-valid-restrict-keys.js +19 -18
  15. package/lib/rules/auth-valid-restrict-to.js +68 -64
  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 +21 -27
  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 +172 -153
  33. package/lib/utils/findFuzzy.js +37 -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,107 @@
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 isRunningWithESLint () {
95
+ return process.argv[0].endsWith('node') && process.argv[1].endsWith('eslint')
96
+ }
97
+
98
+ function checkEntryCriteria (meta, cdscontext) {
99
+ const isTest = Cache.has('test')
100
+ const hasProjectRoots = Cache.has(`roots:${Cache.get('rootpath')}`)
101
+ const isValidFile = isConfiguredFileType(cdscontext.getFilename(), 'FILES')
102
+ const doRootModelChecks = isTest || (hasProjectRoots && (isRunningWithCDSLint() || isRunningWithESLint() || cdscontext.options.includes('show')))
98
103
  // Also lint empty folders (i.e. lintText "" API)
99
104
  const doEnvironmentChecks =
100
- isTest || (isProjectRoot && isRunningWithCdsLint && cdscontext.getSourceCode().lines[0] === "");
101
- return { isTest, isValidFile, doRootModelChecks, doEnvironmentChecks };
105
+ isTest || (hasProjectRoots && isRunningWithCDSLint() && cdscontext.getSourceCode().lines[0] === '')
106
+ return { isTest, isValidFile, doRootModelChecks, doEnvironmentChecks }
102
107
  }
103
108
 
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;
109
+ function setMetaDefaults (meta) {
110
+ if (!meta.severity) meta.severity = constants.DEFAULT_RULE_SEVERITY
111
+ if (meta.docs && !meta.docs.category) meta.docs.category = constants.DEFAULT_RULE_CATEGORY
112
+ if (!meta.model) meta.model = 'parsed'
113
+ return meta
109
114
  }
110
115
 
111
116
  /**
@@ -119,9 +124,8 @@ function setMetaDefaults(meta) {
119
124
  * @param {*} create
120
125
  * @returns
121
126
  */
122
- function createReport(node, cdscontext, meta, create) {
123
- const handlers = create(cdscontext);
124
-
127
+ function createReport (node, cdscontext, meta, create) {
128
+ const handlers = create(cdscontext)
125
129
  /**
126
130
  * TODO: Can these be rewritten to have a visitor? Note, that so far,
127
131
  * rules without a visitor cannot use eslint disable comments
@@ -130,129 +134,144 @@ function createReport(node, cdscontext, meta, create) {
130
134
  * - Environment rules
131
135
  */
132
136
  switch (typeof handlers) {
133
- case "function":
134
- handlers();
135
- break;
137
+ case 'function':
138
+ handlers()
139
+ break
140
+
141
+ case 'object': {
142
+ if (meta.model !== 'none') {
143
+ const model = cdscontext.getModel()
136
144
 
137
- case "object": {
138
- if (meta.model !== "none") {
139
- const model = cdscontext.getModel();
140
145
  if (model) {
141
146
  model.forall((d) => {
142
147
  Object.entries(handlers)
143
148
  .filter(([type, lazy]) => d.is(type))
144
149
  .forEach(([lazy, handler]) => {
145
150
  try {
146
- handler(d);
151
+ handler(d)
147
152
  } catch (err) {
148
- console.log(`Error in rule "${cdscontext.id}" at ${d.name}`, err);
153
+ console.log(`Error in rule "${cdscontext.id}" at ${d.name}`, err)
149
154
  }
150
- });
151
- });
155
+ })
156
+ })
152
157
  }
153
158
  }
154
- break;
159
+ break
155
160
  }
156
161
  }
157
162
  }
158
163
 
159
- function extendContext(node, context, meta) {
160
- if (!Cache.has("test") && !Cache.has("rootpath")) {
161
- Cache.set("rootpath", path.resolve("."));
164
+ function extendContext (node, context, meta) {
165
+ if (!Cache.has('test')) {
166
+ const filePath = context.getFilename()
167
+ const rootPath = filePath && fs.existsSync(filePath) ? getProjectRootPath(filePath) : ''
168
+ if (rootPath) {
169
+ Cache.set('rootpath', rootPath)
170
+ }
162
171
  }
163
172
 
164
173
  const reportWrapper = (r) => {
165
- const line = r.loc ? r.loc.start.line : r.node.loc.start.line;
174
+ const line = r.loc ? r.loc.start.line : r.node.loc.start.line
166
175
  if (!isRuleDisabled(line, context)) {
167
-
168
- if (meta.model === "inferred") {
169
- const file = resolveFilePath(r.file);
176
+ if (meta.model === 'inferred') {
177
+ if (!r.file) {
178
+ console.error(`Rule ${context.id} must return a "file" property in the rule report!`)
179
+ exit(1)
180
+ }
181
+ const file = Cache.get('rootpath') ? resolveFilePath(r.file) : r.file
170
182
  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
- }
183
+ delete r.file
184
+ context.report(r)
185
+ }
186
+ if (r.file) {
187
+ cacheReport(r, file, context, meta)
187
188
  }
188
-
189
189
  } else {
190
- context.report(r);
190
+ context.report(r)
191
191
  }
192
192
  }
193
- };
193
+ }
194
194
 
195
- const descriptors = Object.getOwnPropertyDescriptors(context);
195
+ const descriptors = Object.getOwnPropertyDescriptors(context)
196
196
  descriptors.report = {
197
197
  value: reportWrapper,
198
198
  writable: false,
199
199
  enumerable: true,
200
200
  configurable: false
201
- };
201
+ }
202
202
 
203
- const cdscontext = Object.create(Object.getPrototypeOf(context), descriptors);
203
+ const cdscontext = Object.create(Object.getPrototypeOf(context), descriptors)
204
204
  cdscontext.getModel =
205
- meta.model === "inferred" ? context.parserServices.getInferredCsn : context.parserServices.getParsedCsn;
205
+ meta.model === 'inferred' ? context.parserServices.getInferredCsn : context.parserServices.getParsedCsn
206
206
  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;
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
213
213
  }
214
214
 
215
- function isRuleDisabled(line, cdscontext) {
216
- let isDisabled = false;
215
+ function isRuleDisabled (line, cdscontext) {
216
+ let isDisabled = false
217
217
  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";
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'
222
+ }
223
+ return isDisabled
224
+ }
225
+
226
+ function cacheReport (r, file, context, meta) {
227
+ delete r.file
228
+ if (r.node && r.node.range) {
229
+ r.node.range = []
230
+ }
231
+ if (r.messageId) {
232
+ r.message = meta.messages[r.messageId]
233
+ delete r.message
234
+ }
235
+ if (r) {
236
+ let reports = new Set()
237
+ if (Cache.has(`report:${file}:${context.id}`)) {
238
+ reports = Cache.get(`report:${file}:${context.id}`)
239
+ }
240
+ reports.add(JSON.stringify(r))
241
+ Cache.set(`report:${file}:${context.id}`, reports)
222
242
  }
223
- return isDisabled;
224
243
  }
225
244
 
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 = [];
245
+ function getDisabled (code, sourcecode, line) {
246
+ const listDisabled = []
247
+ const rules = Cache.get('rules')
248
+ const rulesDisabled = Object.keys(rules).reduce((o, key) => ({ ...o, [key]: 'on' }), {})
249
+ let matches = []
231
250
  if (code) {
232
- matches = [...code.matchAll(REGEX_COMMENTS)];
251
+ matches = [...code.matchAll(REGEX_COMMENTS)]
233
252
  if (matches.length > 0) {
234
253
  matches.forEach((match) => {
235
254
  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];
255
+ const index = match.index
256
+ match = match[0]
257
+ if (match.includes('*/')) {
258
+ match = match.split('*/')[0].replace('/*', '')
259
+ } else if (match.includes('//')) {
260
+ match = match.split('//')[1]
242
261
  }
243
262
  if (match) {
244
- match = match.trim();
263
+ match = match.trim()
245
264
  }
246
- ["disable", "enable"].forEach((keyword) => {
247
- const loc = sourcecode.getLocFromIndex(index);
248
- const disableType = match.split(" ")[0];
249
- let disableRules = match.split(`${disableType} `)[1];
265
+ ['disable', 'enable'].forEach((keyword) => {
266
+ const loc = sourcecode.getLocFromIndex(index)
267
+ const disableType = match.split(' ')[0]
268
+ let disableRules = match.split(`${disableType} `)[1]
250
269
  disableRules = disableRules
251
- ? disableRules.split(",").map((rule) => rule.trim())
252
- : Object.keys(rules).map((rule) => `@sap/cds/${rule}`);
253
- let comment = {};
270
+ ? disableRules.split(',').map((rule) => rule.trim())
271
+ : Object.keys(rules).map((rule) => `@sap/cds/${rule}`)
272
+ let comment = {}
254
273
  if ([`eslint-${keyword}`, `eslint-${keyword}-line`, `eslint-${keyword}-next-line`].includes(disableType)) {
255
- comment = disableType.includes("-next-line")
274
+ comment = disableType.includes('-next-line')
256
275
  ? {
257
276
  lineComment: loc.line,
258
277
  lineDisabled: loc.line + 1,
@@ -264,41 +283,41 @@ function getDisabled(code, sourcecode, line) {
264
283
  lineDisabled: loc.line,
265
284
  rules: disableRules,
266
285
  type: keyword
267
- };
268
- if (!disableType.includes("-line")) {
269
- comment.lineDisabled = "EOF";
286
+ }
287
+ if (!disableType.includes('-line')) {
288
+ comment.lineDisabled = 'EOF'
270
289
  }
271
290
  }
272
- listDisabled.push(comment);
273
- });
291
+ listDisabled.push(comment)
292
+ })
274
293
  }
275
- });
294
+ })
276
295
  for (const el of listDisabled.filter(
277
- (d) => d.lineComment > line && (d.lineDisabled === "EOF" || d.lineDisabled === line)
296
+ (d) => d.lineComment > line && (d.lineDisabled === 'EOF' || d.lineDisabled === line)
278
297
  )) {
279
- if (el.lineDisabled === "EOF") {
280
- el.lineDisabled = getLastLine(code);
298
+ if (el.lineDisabled === 'EOF') {
299
+ el.lineDisabled = getLastLine(code)
281
300
  }
282
301
  if (el.rules) {
283
302
  el.rules.forEach((rule) => {
284
- if (el.type === "disable") {
285
- rulesDisabled[rule] = "off";
286
- } else if (el.type === "enable") {
287
- rulesDisabled[rule] = "on";
303
+ if (el.type === 'disable') {
304
+ rulesDisabled[rule] = 'off'
305
+ } else if (el.type === 'enable') {
306
+ rulesDisabled[rule] = 'on'
288
307
  }
289
- });
308
+ })
290
309
  }
291
310
  }
292
311
  }
293
312
  }
294
- return rulesDisabled;
313
+ return rulesDisabled
295
314
  }
296
315
 
297
- function getLastLine(code) {
298
- const lines = typeof code === "string" ? SourceCode.splitLines(code) : code;
299
- return lines.length - 1;
316
+ function getLastLine (code) {
317
+ const lines = typeof code === 'string' ? SourceCode.splitLines(code) : code
318
+ return lines.length - 1
300
319
  }
301
320
 
302
- function resolveFilePath(file) {
303
- return path.isAbsolute(file) ? file : path.join(Cache.get("rootpath"), file);
321
+ function resolveFilePath (file) {
322
+ return path.isAbsolute(file) ? file : path.join(Cache.get('rootpath'), file)
304
323
  }
@@ -7,86 +7,85 @@
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, threshold = Number.MAX_SAFE_INTEGER) => {
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
+ if (!threshold || (threshold && levDist < threshold)) {
42
+ minDistWords.push(word)
43
+ }
46
44
  }
47
45
 
48
46
  if (levDist < minDist) {
49
- minDist = levDist;
50
- minDistWords = [word];
47
+ if (!threshold || (threshold && levDist < threshold)) {
48
+ minDist = levDist
49
+ minDistWords = [word]
50
+ }
51
51
  }
52
52
  }
53
53
 
54
- log && log(`runtime: ${runtime}ms`);
54
+ log && log(`runtime: ${runtime}ms`)
55
55
 
56
- return minDistWords.sort();
56
+ return minDistWords.sort()
57
57
  }
58
58
 
59
59
  const levDistance = (a, b) => {
60
-
61
60
  if (cache[a] && cache[a][b]) {
62
- return cache[a][b];
61
+ return cache[a][b]
63
62
  }
64
63
 
65
64
  if (a.length === 0) {
66
- return addToCache(a, b, b.length);
65
+ return addToCache(a, b, b.length)
67
66
  }
68
67
 
69
68
  if (b.length === 0) {
70
- return addToCache(a, b, a.length);
69
+ return addToCache(a, b, a.length)
71
70
  }
72
71
 
73
- const tail_a = a.substring(1);
74
- const tail_b = b.substring(1);
72
+ const tailA = a.substring(1)
73
+ const tailB = b.substring(1)
75
74
 
76
75
  if (a[0] === b[0]) {
77
- return levDistance(tail_a, tail_b);
76
+ return levDistance(tailA, tailB)
78
77
  }
79
78
 
80
- const lev1 = levDistance(tail_a, b);
81
- const lev2 = levDistance(a, tail_b);
82
- const lev3 = levDistance(tail_a, tail_b);
79
+ const lev1 = levDistance(tailA, b)
80
+ const lev2 = levDistance(a, tailB)
81
+ const lev3 = levDistance(tailA, tailB)
83
82
 
84
- const levDist = Math.min(lev1, lev2, lev3) + 1;
85
- return addToCache(a, b, levDist);
83
+ const levDist = Math.min(lev1, lev2, lev3) + 1
84
+ return addToCache(a, b, levDist)
86
85
  }
87
86
 
88
87
  const addToCache = (a, b, value) => {
89
- cache[a] = cache[a] || {};
90
- cache[a][b] = value;
91
- return value;
88
+ cache[a] = cache[a] || {}
89
+ cache[a][b] = value
90
+ return value
92
91
  }