@sailshq/language-server 0.4.0 → 0.5.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.
- package/SailsParser.js +70 -16
- package/completions/helper-inputs-completion.js +8 -3
- package/completions/model-attributes-completion.js +10 -4
- package/go-to-definitions/go-to-helper-input.js +112 -0
- package/go-to-definitions/go-to-model-attribute.js +302 -0
- package/go-to-definitions/go-to-model.js +4 -3
- package/index.js +11 -3
- package/package.json +1 -1
- package/validators/validate-model-attribute-exist.js +25 -0
- package/validators/validate-model-exist.js +12 -10
- package/validators/validate-required-helper-input.js +3 -2
package/SailsParser.js
CHANGED
|
@@ -229,7 +229,11 @@ class SailsParser {
|
|
|
229
229
|
prop.key?.name === 'attributes' &&
|
|
230
230
|
prop.value?.type === 'ObjectExpression'
|
|
231
231
|
) {
|
|
232
|
-
defaultAttributes = context.#extractObjectLiteral(
|
|
232
|
+
defaultAttributes = context.#extractObjectLiteral(
|
|
233
|
+
prop.value,
|
|
234
|
+
true,
|
|
235
|
+
configCode
|
|
236
|
+
)
|
|
233
237
|
}
|
|
234
238
|
if (
|
|
235
239
|
prop.key?.name === 'models' &&
|
|
@@ -241,7 +245,9 @@ class SailsParser {
|
|
|
241
245
|
inner.value?.type === 'ObjectExpression'
|
|
242
246
|
) {
|
|
243
247
|
defaultAttributes = context.#extractObjectLiteral(
|
|
244
|
-
inner.value
|
|
248
|
+
inner.value,
|
|
249
|
+
true,
|
|
250
|
+
configCode
|
|
245
251
|
)
|
|
246
252
|
}
|
|
247
253
|
}
|
|
@@ -262,7 +268,11 @@ class SailsParser {
|
|
|
262
268
|
prop.key?.name === 'attributes' &&
|
|
263
269
|
prop.value?.type === 'ObjectExpression'
|
|
264
270
|
) {
|
|
265
|
-
defaultAttributes = context.#extractObjectLiteral(
|
|
271
|
+
defaultAttributes = context.#extractObjectLiteral(
|
|
272
|
+
prop.value,
|
|
273
|
+
true,
|
|
274
|
+
configCode
|
|
275
|
+
)
|
|
266
276
|
}
|
|
267
277
|
}
|
|
268
278
|
}
|
|
@@ -279,6 +289,7 @@ class SailsParser {
|
|
|
279
289
|
const name = file.slice(0, -3)
|
|
280
290
|
const modelPath = path.join(dir, file)
|
|
281
291
|
let attributes = {}
|
|
292
|
+
let attributesLine = 0
|
|
282
293
|
const context = this
|
|
283
294
|
|
|
284
295
|
try {
|
|
@@ -302,7 +313,12 @@ class SailsParser {
|
|
|
302
313
|
prop.key?.name === 'attributes' &&
|
|
303
314
|
prop.value?.type === 'ObjectExpression'
|
|
304
315
|
) {
|
|
305
|
-
attributes = context.#extractObjectLiteral(
|
|
316
|
+
attributes = context.#extractObjectLiteral(
|
|
317
|
+
prop.value,
|
|
318
|
+
true,
|
|
319
|
+
code
|
|
320
|
+
)
|
|
321
|
+
attributesLine = context.#getLineNumber(prop.key.start, code)
|
|
306
322
|
}
|
|
307
323
|
}
|
|
308
324
|
}
|
|
@@ -315,7 +331,11 @@ class SailsParser {
|
|
|
315
331
|
node.left.property.name === 'attributes' &&
|
|
316
332
|
node.right.type === 'ObjectExpression'
|
|
317
333
|
) {
|
|
318
|
-
attributes = context.#extractObjectLiteral(node.right)
|
|
334
|
+
attributes = context.#extractObjectLiteral(node.right, true, code)
|
|
335
|
+
attributesLine = context.#getLineNumber(
|
|
336
|
+
node.left.property.start,
|
|
337
|
+
code
|
|
338
|
+
)
|
|
319
339
|
}
|
|
320
340
|
},
|
|
321
341
|
ExportDefaultDeclaration(node) {
|
|
@@ -325,7 +345,12 @@ class SailsParser {
|
|
|
325
345
|
prop.key?.name === 'attributes' &&
|
|
326
346
|
prop.value?.type === 'ObjectExpression'
|
|
327
347
|
) {
|
|
328
|
-
attributes = context.#extractObjectLiteral(
|
|
348
|
+
attributes = context.#extractObjectLiteral(
|
|
349
|
+
prop.value,
|
|
350
|
+
true,
|
|
351
|
+
code
|
|
352
|
+
)
|
|
353
|
+
attributesLine = context.#getLineNumber(prop.key.start, code)
|
|
329
354
|
}
|
|
330
355
|
}
|
|
331
356
|
}
|
|
@@ -339,13 +364,22 @@ class SailsParser {
|
|
|
339
364
|
// Merge defaultAttributes first, then model attributes (model overrides default)
|
|
340
365
|
const mergedAttributes = {}
|
|
341
366
|
for (const key of Object.keys(defaultAttributes)) {
|
|
342
|
-
|
|
367
|
+
const attr = defaultAttributes[key]
|
|
368
|
+
mergedAttributes[key] = {
|
|
369
|
+
...attr,
|
|
370
|
+
path: modelsConfigPath
|
|
371
|
+
}
|
|
343
372
|
}
|
|
344
373
|
for (const key of Object.keys(attributes)) {
|
|
345
|
-
|
|
374
|
+
const attr = attributes[key]
|
|
375
|
+
mergedAttributes[key] = {
|
|
376
|
+
...attr,
|
|
377
|
+
path: modelPath
|
|
378
|
+
}
|
|
346
379
|
}
|
|
347
380
|
models[name] = {
|
|
348
381
|
path: modelPath,
|
|
382
|
+
attributesLine,
|
|
349
383
|
methods: STATIC_METHODS,
|
|
350
384
|
chainableMethods: CHAINABLE_METHODS,
|
|
351
385
|
attributes: mergedAttributes
|
|
@@ -482,7 +516,11 @@ class SailsParser {
|
|
|
482
516
|
prop.key.name === 'inputs' &&
|
|
483
517
|
prop.value.type === 'ObjectExpression'
|
|
484
518
|
) {
|
|
485
|
-
inputs = context.#extractObjectLiteral(
|
|
519
|
+
inputs = context.#extractObjectLiteral(
|
|
520
|
+
prop.value,
|
|
521
|
+
true,
|
|
522
|
+
content
|
|
523
|
+
)
|
|
486
524
|
}
|
|
487
525
|
if (
|
|
488
526
|
prop.key &&
|
|
@@ -513,7 +551,11 @@ class SailsParser {
|
|
|
513
551
|
prop.key.name === 'inputs' &&
|
|
514
552
|
prop.value.type === 'ObjectExpression'
|
|
515
553
|
) {
|
|
516
|
-
inputs = context.#extractObjectLiteral(
|
|
554
|
+
inputs = context.#extractObjectLiteral(
|
|
555
|
+
prop.value,
|
|
556
|
+
true,
|
|
557
|
+
content
|
|
558
|
+
)
|
|
517
559
|
}
|
|
518
560
|
if (
|
|
519
561
|
prop.key &&
|
|
@@ -539,14 +581,13 @@ class SailsParser {
|
|
|
539
581
|
const match = content.match(/inputs\s*:\s*\{([\s\S]*?)\n\s*\}/m)
|
|
540
582
|
if (match) {
|
|
541
583
|
try {
|
|
542
|
-
// Try to parse as JS object
|
|
543
584
|
const fakeObj = `({${match[1]}})`
|
|
544
585
|
const ast = acorn.parse(fakeObj, { ecmaVersion: 'latest' })
|
|
545
586
|
let obj = {}
|
|
546
587
|
walk.simple(ast, {
|
|
547
588
|
ObjectExpression(node) {
|
|
548
589
|
if (!obj || Object.keys(obj).length === 0) {
|
|
549
|
-
obj = context.#extractObjectLiteral(node)
|
|
590
|
+
obj = context.#extractObjectLiteral(node, true, content)
|
|
550
591
|
}
|
|
551
592
|
}
|
|
552
593
|
})
|
|
@@ -574,7 +615,7 @@ class SailsParser {
|
|
|
574
615
|
return helpers
|
|
575
616
|
}
|
|
576
617
|
|
|
577
|
-
#extractObjectLiteral(node) {
|
|
618
|
+
#extractObjectLiteral(node, withLineNumbers = false, sourceCode = null) {
|
|
578
619
|
if (node.type !== 'ObjectExpression') return undefined
|
|
579
620
|
const obj = {}
|
|
580
621
|
for (const prop of node.properties) {
|
|
@@ -583,11 +624,15 @@ class SailsParser {
|
|
|
583
624
|
prop.key.type === 'Identifier' ? prop.key.name : prop.key.value
|
|
584
625
|
let value
|
|
585
626
|
if (prop.value.type === 'ObjectExpression') {
|
|
586
|
-
value = this.#extractObjectLiteral(
|
|
627
|
+
value = this.#extractObjectLiteral(
|
|
628
|
+
prop.value,
|
|
629
|
+
withLineNumbers,
|
|
630
|
+
sourceCode
|
|
631
|
+
)
|
|
587
632
|
} else if (prop.value.type === 'ArrayExpression') {
|
|
588
633
|
value = prop.value.elements.map((el) =>
|
|
589
634
|
el.type === 'ObjectExpression'
|
|
590
|
-
? this.#extractObjectLiteral(el)
|
|
635
|
+
? this.#extractObjectLiteral(el, withLineNumbers, sourceCode)
|
|
591
636
|
: el.type === 'Literal'
|
|
592
637
|
? el.value
|
|
593
638
|
: el.type === 'Identifier'
|
|
@@ -601,11 +646,20 @@ class SailsParser {
|
|
|
601
646
|
} else {
|
|
602
647
|
value = undefined
|
|
603
648
|
}
|
|
604
|
-
|
|
649
|
+
if (withLineNumbers && sourceCode) {
|
|
650
|
+
const line = this.#getLineNumber(prop.key.start, sourceCode)
|
|
651
|
+
obj[key] = { value, line }
|
|
652
|
+
} else {
|
|
653
|
+
obj[key] = value
|
|
654
|
+
}
|
|
605
655
|
}
|
|
606
656
|
}
|
|
607
657
|
return obj
|
|
608
658
|
}
|
|
659
|
+
#getLineNumber(offset, sourceCode) {
|
|
660
|
+
const lines = sourceCode.substring(0, offset).split('\n')
|
|
661
|
+
return lines.length
|
|
662
|
+
}
|
|
609
663
|
#getDataTypes() {
|
|
610
664
|
return [
|
|
611
665
|
{
|
|
@@ -13,9 +13,14 @@ function getHelperPath(line) {
|
|
|
13
13
|
function getInputCompletionItems(inputsObj) {
|
|
14
14
|
if (!inputsObj || typeof inputsObj !== 'object') return []
|
|
15
15
|
return Object.entries(inputsObj).map(([inputName, inputDef]) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const typeValue = inputDef?.value?.type?.value ?? inputDef?.type
|
|
17
|
+
const requiredValue = inputDef?.value?.required?.value ?? inputDef?.required
|
|
18
|
+
const descriptionValue =
|
|
19
|
+
inputDef?.value?.description?.value ?? inputDef?.description
|
|
20
|
+
|
|
21
|
+
let type = typeValue
|
|
22
|
+
let required = requiredValue ? 'required' : 'optional'
|
|
23
|
+
let description = descriptionValue || ''
|
|
19
24
|
let detail = type ? `${type} (${required})` : required
|
|
20
25
|
return {
|
|
21
26
|
label: inputName,
|
|
@@ -274,6 +274,7 @@ module.exports = function modelAttributesCompletion(
|
|
|
274
274
|
let hasAttributes = false
|
|
275
275
|
let hasQueryOptions = false
|
|
276
276
|
let hasOperators = false
|
|
277
|
+
let hasModifiers = false
|
|
277
278
|
for (const prop of objNode.properties) {
|
|
278
279
|
if (!prop.key) continue
|
|
279
280
|
const keyName = prop.key.name || prop.key.value
|
|
@@ -281,7 +282,9 @@ module.exports = function modelAttributesCompletion(
|
|
|
281
282
|
hasQueryOptions = true
|
|
282
283
|
} else if (WATERLINE_OPERATORS.includes(keyName)) {
|
|
283
284
|
hasOperators = true
|
|
284
|
-
} else if (
|
|
285
|
+
} else if (WATERLINE_MODIFIERS.includes(keyName)) {
|
|
286
|
+
hasModifiers = true
|
|
287
|
+
} else {
|
|
285
288
|
// Check if it's a valid attribute
|
|
286
289
|
const model = getModelByName(astModelName)
|
|
287
290
|
if (
|
|
@@ -303,12 +306,13 @@ module.exports = function modelAttributesCompletion(
|
|
|
303
306
|
// - If we have query options (like where, select, limit), this is a query options object
|
|
304
307
|
// - If we're explicitly inside a where clause (isInsideWhere), show attributes
|
|
305
308
|
// - If we have attributes but no query options, it's a criteria object
|
|
309
|
+
// - Modifiers at top level don't make this a criteria object (only inside them)
|
|
306
310
|
const isCriteriaMode =
|
|
307
311
|
isInsideWhere || (hasAttributes && !hasQueryOptions)
|
|
308
312
|
|
|
309
|
-
// If we have query options and we're not inside where, this is NOT a criteria context
|
|
313
|
+
// If we have query options or modifiers and we're not inside where, this is NOT a criteria context
|
|
310
314
|
// Don't show attribute completions at the query options level
|
|
311
|
-
if (hasQueryOptions && !isInsideWhere) {
|
|
315
|
+
if ((hasQueryOptions || hasModifiers) && !isInsideWhere) {
|
|
312
316
|
// But we still need to check if we're typing a new key after existing query options
|
|
313
317
|
// Check if cursor is in a position to type a new key
|
|
314
318
|
const isTypingNewKey =
|
|
@@ -352,7 +356,7 @@ module.exports = function modelAttributesCompletion(
|
|
|
352
356
|
}
|
|
353
357
|
}
|
|
354
358
|
|
|
355
|
-
// If it's a modifier (or/and/not), check inside the array
|
|
359
|
+
// If it's a modifier (or/and/not), check inside the array or object at any level
|
|
356
360
|
if (WATERLINE_MODIFIERS.includes(keyName)) {
|
|
357
361
|
if (
|
|
358
362
|
prop.value &&
|
|
@@ -364,6 +368,8 @@ module.exports = function modelAttributesCompletion(
|
|
|
364
368
|
if (checkObjectForCursor(el, true)) return true
|
|
365
369
|
}
|
|
366
370
|
}
|
|
371
|
+
} else if (prop.value && prop.value.type === 'ObjectExpression') {
|
|
372
|
+
if (checkObjectForCursor(prop.value, true)) return true
|
|
367
373
|
}
|
|
368
374
|
}
|
|
369
375
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
const acorn = require('acorn')
|
|
3
|
+
const walk = require('acorn-walk')
|
|
4
|
+
|
|
5
|
+
module.exports = async function goToHelperInput(document, position, typeMap) {
|
|
6
|
+
const text = document.getText()
|
|
7
|
+
const offset = document.offsetAt(position)
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const ast = acorn.parse(text, {
|
|
11
|
+
ecmaVersion: 'latest',
|
|
12
|
+
sourceType: 'module'
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
let result = null
|
|
16
|
+
|
|
17
|
+
walk.simple(ast, {
|
|
18
|
+
CallExpression(node) {
|
|
19
|
+
if (
|
|
20
|
+
node.callee &&
|
|
21
|
+
node.callee.type === 'MemberExpression' &&
|
|
22
|
+
node.callee.property.name === 'with' &&
|
|
23
|
+
node.callee.object &&
|
|
24
|
+
node.callee.object.type === 'MemberExpression'
|
|
25
|
+
) {
|
|
26
|
+
const helperPath = extractHelperPath(node.callee.object)
|
|
27
|
+
if (!helperPath) return
|
|
28
|
+
|
|
29
|
+
const helperInfo = typeMap.helpers && typeMap.helpers[helperPath]
|
|
30
|
+
if (!helperInfo || !helperInfo.inputs) return
|
|
31
|
+
|
|
32
|
+
const objArg = node.arguments[0]
|
|
33
|
+
if (!objArg || objArg.type !== 'ObjectExpression') return
|
|
34
|
+
|
|
35
|
+
for (const prop of objArg.properties) {
|
|
36
|
+
if (prop.type !== 'Property' || !prop.key) continue
|
|
37
|
+
|
|
38
|
+
const inputName =
|
|
39
|
+
prop.key.type === 'Identifier'
|
|
40
|
+
? prop.key.name
|
|
41
|
+
: prop.key.type === 'Literal'
|
|
42
|
+
? prop.key.value
|
|
43
|
+
: null
|
|
44
|
+
|
|
45
|
+
if (!inputName) continue
|
|
46
|
+
|
|
47
|
+
const keyStart = prop.key.start
|
|
48
|
+
const keyEnd = prop.key.end
|
|
49
|
+
|
|
50
|
+
if (offset >= keyStart && offset <= keyEnd) {
|
|
51
|
+
const inputInfo = helperInfo.inputs[inputName]
|
|
52
|
+
if (inputInfo?.line) {
|
|
53
|
+
const uri = `file://${helperInfo.path}`
|
|
54
|
+
result = lsp.LocationLink.create(
|
|
55
|
+
uri,
|
|
56
|
+
lsp.Range.create(
|
|
57
|
+
inputInfo.line - 1,
|
|
58
|
+
0,
|
|
59
|
+
inputInfo.line - 1,
|
|
60
|
+
0
|
|
61
|
+
),
|
|
62
|
+
lsp.Range.create(
|
|
63
|
+
inputInfo.line - 1,
|
|
64
|
+
0,
|
|
65
|
+
inputInfo.line - 1,
|
|
66
|
+
0
|
|
67
|
+
),
|
|
68
|
+
lsp.Range.create(
|
|
69
|
+
document.positionAt(keyStart),
|
|
70
|
+
document.positionAt(keyEnd)
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractHelperPath(node) {
|
|
88
|
+
const segments = []
|
|
89
|
+
let current = node
|
|
90
|
+
|
|
91
|
+
while (current && current.type === 'MemberExpression') {
|
|
92
|
+
if (current.property && current.property.type === 'Identifier') {
|
|
93
|
+
const propName = current.property.name
|
|
94
|
+
if (propName === 'helpers') {
|
|
95
|
+
if (
|
|
96
|
+
current.object &&
|
|
97
|
+
current.object.type === 'Identifier' &&
|
|
98
|
+
current.object.name === 'sails'
|
|
99
|
+
) {
|
|
100
|
+
const toKebab = (s) =>
|
|
101
|
+
s.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
|
102
|
+
return segments.map(toKebab).join('/')
|
|
103
|
+
}
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
segments.unshift(propName)
|
|
107
|
+
}
|
|
108
|
+
current = current.object
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
const acorn = require('acorn')
|
|
3
|
+
const walk = require('acorn-walk')
|
|
4
|
+
|
|
5
|
+
module.exports = async function goToModelAttribute(
|
|
6
|
+
document,
|
|
7
|
+
position,
|
|
8
|
+
typeMap
|
|
9
|
+
) {
|
|
10
|
+
const text = document.getText()
|
|
11
|
+
const offset = document.offsetAt(position)
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const ast = acorn.parse(text, {
|
|
15
|
+
ecmaVersion: 'latest',
|
|
16
|
+
sourceType: 'module'
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
let result = null
|
|
20
|
+
|
|
21
|
+
const checkPropertyForAttribute = (prop, model, document) => {
|
|
22
|
+
if (prop.type !== 'Property' || !prop.key) return null
|
|
23
|
+
|
|
24
|
+
const attrName =
|
|
25
|
+
prop.key.type === 'Identifier'
|
|
26
|
+
? prop.key.name
|
|
27
|
+
: prop.key.type === 'Literal'
|
|
28
|
+
? prop.key.value
|
|
29
|
+
: null
|
|
30
|
+
|
|
31
|
+
if (!attrName) return null
|
|
32
|
+
|
|
33
|
+
const keyStart = prop.key.start
|
|
34
|
+
const keyEnd = prop.key.end
|
|
35
|
+
|
|
36
|
+
if (offset >= keyStart && offset <= keyEnd) {
|
|
37
|
+
const attrInfo = model.attributes?.[attrName]
|
|
38
|
+
if (attrInfo?.line && attrInfo?.path) {
|
|
39
|
+
const uri = `file://${attrInfo.path}`
|
|
40
|
+
return lsp.LocationLink.create(
|
|
41
|
+
uri,
|
|
42
|
+
lsp.Range.create(attrInfo.line - 1, 0, attrInfo.line - 1, 0),
|
|
43
|
+
lsp.Range.create(attrInfo.line - 1, 0, attrInfo.line - 1, 0),
|
|
44
|
+
lsp.Range.create(
|
|
45
|
+
document.positionAt(keyStart),
|
|
46
|
+
document.positionAt(keyEnd)
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const checkStringLiteralForAttribute = (literal, model, document) => {
|
|
55
|
+
if (literal.type !== 'Literal' || typeof literal.value !== 'string')
|
|
56
|
+
return null
|
|
57
|
+
|
|
58
|
+
const attrName = literal.value
|
|
59
|
+
const literalStart = literal.start
|
|
60
|
+
const literalEnd = literal.end
|
|
61
|
+
|
|
62
|
+
if (offset >= literalStart && offset <= literalEnd) {
|
|
63
|
+
const attrInfo = model.attributes?.[attrName]
|
|
64
|
+
if (attrInfo?.line && attrInfo?.path) {
|
|
65
|
+
const uri = `file://${attrInfo.path}`
|
|
66
|
+
return lsp.LocationLink.create(
|
|
67
|
+
uri,
|
|
68
|
+
lsp.Range.create(attrInfo.line - 1, 0, attrInfo.line - 1, 0),
|
|
69
|
+
lsp.Range.create(attrInfo.line - 1, 0, attrInfo.line - 1, 0),
|
|
70
|
+
lsp.Range.create(
|
|
71
|
+
document.positionAt(literalStart),
|
|
72
|
+
document.positionAt(literalEnd)
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const checkArrayForAttributes = (arrayNode, model, document) => {
|
|
81
|
+
if (!arrayNode || arrayNode.type !== 'ArrayExpression') return null
|
|
82
|
+
|
|
83
|
+
for (const element of arrayNode.elements) {
|
|
84
|
+
if (element?.type === 'Literal' && typeof element.value === 'string') {
|
|
85
|
+
const res = checkStringLiteralForAttribute(element, model, document)
|
|
86
|
+
if (res) return res
|
|
87
|
+
} else if (element?.type === 'ObjectExpression') {
|
|
88
|
+
const res = traverseObjectExpression(element, model, document)
|
|
89
|
+
if (res) return res
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const traverseObjectExpression = (objNode, model, document) => {
|
|
96
|
+
if (!objNode || objNode.type !== 'ObjectExpression') return null
|
|
97
|
+
|
|
98
|
+
for (const prop of objNode.properties) {
|
|
99
|
+
if (prop.type !== 'Property') continue
|
|
100
|
+
|
|
101
|
+
const propName =
|
|
102
|
+
prop.key.type === 'Identifier'
|
|
103
|
+
? prop.key.name
|
|
104
|
+
: prop.key.type === 'Literal'
|
|
105
|
+
? prop.key.value
|
|
106
|
+
: null
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
propName === 'where' ||
|
|
110
|
+
propName === 'or' ||
|
|
111
|
+
propName === 'and' ||
|
|
112
|
+
propName === 'not'
|
|
113
|
+
) {
|
|
114
|
+
if (prop.value.type === 'ObjectExpression') {
|
|
115
|
+
const res = traverseObjectExpression(prop.value, model, document)
|
|
116
|
+
if (res) return res
|
|
117
|
+
} else if (prop.value.type === 'ArrayExpression') {
|
|
118
|
+
for (const element of prop.value.elements) {
|
|
119
|
+
const res = traverseObjectExpression(element, model, document)
|
|
120
|
+
if (res) return res
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else if (propName === 'select' || propName === 'omit') {
|
|
124
|
+
if (prop.value.type === 'ArrayExpression') {
|
|
125
|
+
const res = checkArrayForAttributes(prop.value, model, document)
|
|
126
|
+
if (res) return res
|
|
127
|
+
}
|
|
128
|
+
} else if (propName === 'sort') {
|
|
129
|
+
if (prop.value.type === 'ArrayExpression') {
|
|
130
|
+
const res = checkArrayForAttributes(prop.value, model, document)
|
|
131
|
+
if (res) return res
|
|
132
|
+
} else if (
|
|
133
|
+
prop.value.type === 'Literal' &&
|
|
134
|
+
typeof prop.value.value === 'string'
|
|
135
|
+
) {
|
|
136
|
+
const sortString = prop.value.value
|
|
137
|
+
const attrMatch = sortString.match(/^(\w+)/)
|
|
138
|
+
if (attrMatch) {
|
|
139
|
+
const attrName = attrMatch[1]
|
|
140
|
+
const attrInfo = model.attributes?.[attrName]
|
|
141
|
+
if (attrInfo?.line && attrInfo?.path) {
|
|
142
|
+
const sortStart = prop.value.start
|
|
143
|
+
const sortEnd = prop.value.end
|
|
144
|
+
if (offset >= sortStart && offset <= sortEnd) {
|
|
145
|
+
const uri = `file://${attrInfo.path}`
|
|
146
|
+
return lsp.LocationLink.create(
|
|
147
|
+
uri,
|
|
148
|
+
lsp.Range.create(
|
|
149
|
+
attrInfo.line - 1,
|
|
150
|
+
0,
|
|
151
|
+
attrInfo.line - 1,
|
|
152
|
+
0
|
|
153
|
+
),
|
|
154
|
+
lsp.Range.create(
|
|
155
|
+
attrInfo.line - 1,
|
|
156
|
+
0,
|
|
157
|
+
attrInfo.line - 1,
|
|
158
|
+
0
|
|
159
|
+
),
|
|
160
|
+
lsp.Range.create(
|
|
161
|
+
document.positionAt(sortStart),
|
|
162
|
+
document.positionAt(sortEnd)
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
const res = checkPropertyForAttribute(prop, model, document)
|
|
171
|
+
if (res) return res
|
|
172
|
+
|
|
173
|
+
if (prop.value.type === 'ObjectExpression') {
|
|
174
|
+
const nestedRes = traverseObjectExpression(
|
|
175
|
+
prop.value,
|
|
176
|
+
model,
|
|
177
|
+
document
|
|
178
|
+
)
|
|
179
|
+
if (nestedRes) return nestedRes
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const findModelInChain = (node) => {
|
|
187
|
+
let current = node
|
|
188
|
+
while (current) {
|
|
189
|
+
if (
|
|
190
|
+
current.type === 'CallExpression' &&
|
|
191
|
+
current.callee?.type === 'MemberExpression'
|
|
192
|
+
) {
|
|
193
|
+
if (current.callee.object?.type === 'Identifier') {
|
|
194
|
+
const modelName = current.callee.object.name
|
|
195
|
+
return typeMap.models?.[modelName]
|
|
196
|
+
}
|
|
197
|
+
current = current.callee.object
|
|
198
|
+
} else if (current.type === 'MemberExpression') {
|
|
199
|
+
current = current.object
|
|
200
|
+
} else {
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
walk.simple(ast, {
|
|
208
|
+
CallExpression(node) {
|
|
209
|
+
if (node.callee.type === 'MemberExpression') {
|
|
210
|
+
const methodName =
|
|
211
|
+
node.callee.property?.type === 'Identifier'
|
|
212
|
+
? node.callee.property.name
|
|
213
|
+
: null
|
|
214
|
+
|
|
215
|
+
if (methodName === 'select' || methodName === 'omit') {
|
|
216
|
+
const model = findModelInChain(node.callee.object)
|
|
217
|
+
if (model) {
|
|
218
|
+
const firstArg = node.arguments?.[0]
|
|
219
|
+
if (firstArg?.type === 'ArrayExpression') {
|
|
220
|
+
for (const element of firstArg.elements) {
|
|
221
|
+
const res = checkStringLiteralForAttribute(
|
|
222
|
+
element,
|
|
223
|
+
model,
|
|
224
|
+
document
|
|
225
|
+
)
|
|
226
|
+
if (res) {
|
|
227
|
+
result = res
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else if (methodName === 'sort') {
|
|
234
|
+
const model = findModelInChain(node.callee.object)
|
|
235
|
+
if (model) {
|
|
236
|
+
const firstArg = node.arguments?.[0]
|
|
237
|
+
if (firstArg?.type === 'ArrayExpression') {
|
|
238
|
+
const res = checkArrayForAttributes(firstArg, model, document)
|
|
239
|
+
if (res) {
|
|
240
|
+
result = res
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
} else if (
|
|
244
|
+
firstArg?.type === 'Literal' &&
|
|
245
|
+
typeof firstArg.value === 'string'
|
|
246
|
+
) {
|
|
247
|
+
const sortString = firstArg.value
|
|
248
|
+
const attrMatch = sortString.match(/^(\w+)/)
|
|
249
|
+
if (attrMatch) {
|
|
250
|
+
const attrName = attrMatch[1]
|
|
251
|
+
const attrInfo = model.attributes?.[attrName]
|
|
252
|
+
if (attrInfo?.line && attrInfo?.path) {
|
|
253
|
+
const sortStart = firstArg.start
|
|
254
|
+
const sortEnd = firstArg.end
|
|
255
|
+
if (offset >= sortStart && offset <= sortEnd) {
|
|
256
|
+
const uri = `file://${attrInfo.path}`
|
|
257
|
+
result = lsp.LocationLink.create(
|
|
258
|
+
uri,
|
|
259
|
+
lsp.Range.create(
|
|
260
|
+
attrInfo.line - 1,
|
|
261
|
+
0,
|
|
262
|
+
attrInfo.line - 1,
|
|
263
|
+
0
|
|
264
|
+
),
|
|
265
|
+
lsp.Range.create(
|
|
266
|
+
attrInfo.line - 1,
|
|
267
|
+
0,
|
|
268
|
+
attrInfo.line - 1,
|
|
269
|
+
0
|
|
270
|
+
),
|
|
271
|
+
lsp.Range.create(
|
|
272
|
+
document.positionAt(sortStart),
|
|
273
|
+
document.positionAt(sortEnd)
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} else if (node.callee.object.type === 'Identifier') {
|
|
283
|
+
const modelName = node.callee.object.name
|
|
284
|
+
const model = typeMap.models?.[modelName]
|
|
285
|
+
|
|
286
|
+
if (!model) return
|
|
287
|
+
|
|
288
|
+
const firstArg = node.arguments?.[0]
|
|
289
|
+
if (firstArg?.type === 'ObjectExpression') {
|
|
290
|
+
const res = traverseObjectExpression(firstArg, model, document)
|
|
291
|
+
if (res) result = res
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
return result
|
|
299
|
+
} catch (error) {
|
|
300
|
+
return null
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -25,12 +25,13 @@ module.exports = async function goToModel(document, position, typeMap) {
|
|
|
25
25
|
const model = typeMap.models?.[modelName]
|
|
26
26
|
if (!model?.path) return null
|
|
27
27
|
|
|
28
|
+
const targetLine = model.attributesLine ? model.attributesLine - 1 : 0
|
|
28
29
|
const uri = `file://${model.path}`
|
|
29
30
|
return lsp.LocationLink.create(
|
|
30
31
|
uri,
|
|
31
|
-
lsp.Range.create(
|
|
32
|
-
lsp.Range.create(
|
|
33
|
-
lsp.Range.create(document.positionAt(start), document.positionAt(end))
|
|
32
|
+
lsp.Range.create(targetLine, 0, targetLine, 0),
|
|
33
|
+
lsp.Range.create(targetLine, 0, targetLine, 0),
|
|
34
|
+
lsp.Range.create(document.positionAt(start), document.positionAt(end))
|
|
34
35
|
)
|
|
35
36
|
}
|
|
36
37
|
}
|
package/index.js
CHANGED
|
@@ -11,7 +11,9 @@ const goToView = require('./go-to-definitions/go-to-view')
|
|
|
11
11
|
const goToPage = require('./go-to-definitions/go-to-page')
|
|
12
12
|
const goToPolicy = require('./go-to-definitions/go-to-policy')
|
|
13
13
|
const goToHelper = require('./go-to-definitions/go-to-helper')
|
|
14
|
+
const goToHelperInput = require('./go-to-definitions/go-to-helper-input')
|
|
14
15
|
const goToModel = require('./go-to-definitions/go-to-model')
|
|
16
|
+
const goToModelAttribute = require('./go-to-definitions/go-to-model-attribute')
|
|
15
17
|
|
|
16
18
|
// Completions
|
|
17
19
|
const actionsCompletion = require('./completions/actions-completion')
|
|
@@ -83,14 +85,18 @@ connection.onDefinition(async (params) => {
|
|
|
83
85
|
pageDefinition,
|
|
84
86
|
policyDefinition,
|
|
85
87
|
helperDefinition,
|
|
86
|
-
|
|
88
|
+
helperInputDefinition,
|
|
89
|
+
modelDefinition,
|
|
90
|
+
modelAttributeDefinition
|
|
87
91
|
] = await Promise.all([
|
|
88
92
|
goToAction(document, params.position, typeMap),
|
|
89
93
|
goToView(document, params.position, typeMap),
|
|
90
94
|
goToPage(document, params.position, typeMap),
|
|
91
95
|
goToPolicy(document, params.position, typeMap),
|
|
92
96
|
goToHelper(document, params.position, typeMap),
|
|
93
|
-
|
|
97
|
+
goToHelperInput(document, params.position, typeMap),
|
|
98
|
+
goToModel(document, params.position, typeMap),
|
|
99
|
+
goToModelAttribute(document, params.position, typeMap)
|
|
94
100
|
])
|
|
95
101
|
|
|
96
102
|
const definitions = [
|
|
@@ -99,7 +105,9 @@ connection.onDefinition(async (params) => {
|
|
|
99
105
|
pageDefinition,
|
|
100
106
|
policyDefinition,
|
|
101
107
|
helperDefinition,
|
|
102
|
-
|
|
108
|
+
helperInputDefinition,
|
|
109
|
+
modelDefinition,
|
|
110
|
+
modelAttributeDefinition
|
|
103
111
|
].filter((def) => def && (Array.isArray(def) ? def.length > 0 : true))
|
|
104
112
|
return definitions
|
|
105
113
|
})
|
package/package.json
CHANGED
|
@@ -61,6 +61,14 @@ function validateCriteriaAttributes(
|
|
|
61
61
|
)
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
} else if (prop.value && prop.value.type === 'ObjectExpression') {
|
|
65
|
+
validateCriteriaAttributes(
|
|
66
|
+
prop.value,
|
|
67
|
+
model,
|
|
68
|
+
document,
|
|
69
|
+
diagnostics,
|
|
70
|
+
effectiveModelName
|
|
71
|
+
)
|
|
64
72
|
}
|
|
65
73
|
continue
|
|
66
74
|
}
|
|
@@ -455,6 +463,23 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
|
|
|
455
463
|
effectiveModelName
|
|
456
464
|
)
|
|
457
465
|
continue
|
|
466
|
+
} else if (
|
|
467
|
+
WATERLINE_MODIFIERS.includes(attribute) &&
|
|
468
|
+
prop.value &&
|
|
469
|
+
prop.value.type === 'ArrayExpression'
|
|
470
|
+
) {
|
|
471
|
+
for (const el of prop.value.elements) {
|
|
472
|
+
if (el && el.type === 'ObjectExpression') {
|
|
473
|
+
validateCriteriaAttributes(
|
|
474
|
+
el,
|
|
475
|
+
model,
|
|
476
|
+
document,
|
|
477
|
+
diagnostics,
|
|
478
|
+
effectiveModelName
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
continue
|
|
458
483
|
}
|
|
459
484
|
if (
|
|
460
485
|
(attribute === 'select' || attribute === 'omit') &&
|
|
@@ -8,16 +8,18 @@ const lsp = require('vscode-languageserver/node')
|
|
|
8
8
|
module.exports = function validateModelExist(document, typeMap) {
|
|
9
9
|
const diagnostics = []
|
|
10
10
|
const text = document.getText()
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
modelMap[key.toLowerCase()] = typeMap.models[key]
|
|
16
|
-
}
|
|
11
|
+
const models = typeMap.models || {}
|
|
12
|
+
const lowercasedModelMap = {}
|
|
13
|
+
for (const key of Object.keys(models)) {
|
|
14
|
+
lowercasedModelMap[key.toLowerCase()] = models[key]
|
|
17
15
|
}
|
|
18
16
|
function modelExists(name) {
|
|
19
17
|
if (!name) return false
|
|
20
|
-
return !!
|
|
18
|
+
return !!models[name]
|
|
19
|
+
}
|
|
20
|
+
function modelExistsLowercased(name) {
|
|
21
|
+
if (!name) return false
|
|
22
|
+
return !!lowercasedModelMap[name.toLowerCase()]
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
const knownGlobals = [
|
|
@@ -30,9 +32,9 @@ module.exports = function validateModelExist(document, typeMap) {
|
|
|
30
32
|
'process'
|
|
31
33
|
]
|
|
32
34
|
|
|
33
|
-
// User.find() or User.create() etc
|
|
35
|
+
// User.find() or User.create() etc (only PascalCase identifiers)
|
|
34
36
|
const modelCallRegex =
|
|
35
|
-
/\b([A-Za-z0-9_]
|
|
37
|
+
/\b([A-Z][A-Za-z0-9_]*)\s*\.(?:find|findOne|create|createEach|update|destroy|count|sum|where|findOrCreate)\s*\(/g
|
|
36
38
|
let match
|
|
37
39
|
while ((match = modelCallRegex.exec(text)) !== null) {
|
|
38
40
|
const modelName = match[1]
|
|
@@ -58,7 +60,7 @@ module.exports = function validateModelExist(document, typeMap) {
|
|
|
58
60
|
/sails\.models\.([A-Za-z0-9_]+)\s*\.(?:find|findOne|create|createEach|update|destroy|count|sum|where|findOrCreate)\s*\(/g
|
|
59
61
|
while ((match = sailsModelCallRegex.exec(text)) !== null) {
|
|
60
62
|
const modelName = match[1]
|
|
61
|
-
if (!
|
|
63
|
+
if (!modelExistsLowercased(modelName)) {
|
|
62
64
|
diagnostics.push(
|
|
63
65
|
lsp.Diagnostic.create(
|
|
64
66
|
lsp.Range.create(
|
|
@@ -49,9 +49,10 @@ module.exports = function validateRequiredHelperInput(document, typeMap) {
|
|
|
49
49
|
for (const [inputKey, inputDef] of Object.entries(
|
|
50
50
|
helperInfo.inputs
|
|
51
51
|
)) {
|
|
52
|
+
const requiredValue =
|
|
53
|
+
inputDef?.value?.required?.value ?? inputDef?.required
|
|
52
54
|
const isRequired =
|
|
53
|
-
|
|
54
|
-
(inputDef.required === true || inputDef.required === 'true')
|
|
55
|
+
requiredValue === true || requiredValue === 'true'
|
|
55
56
|
if (isRequired && !providedKeys.has(inputKey)) {
|
|
56
57
|
diagnostics.push(
|
|
57
58
|
lsp.Diagnostic.create(
|