@sailshq/language-server 0.3.2 → 0.5.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.
- package/SailsParser.js +125 -33
- package/completions/helper-inputs-completion.js +8 -3
- package/completions/model-attribute-props-completion.js +20 -19
- package/completions/model-attributes-completion.js +356 -0
- package/completions/model-methods-completion.js +24 -1
- package/go-to-definitions/go-to-action.js +82 -29
- 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/go-to-definitions/go-to-page.js +40 -26
- package/go-to-definitions/go-to-view.js +42 -24
- package/index.js +12 -4
- package/package.json +1 -1
- package/validators/validate-action-exist.js +54 -15
- package/validators/validate-data-type.js +88 -25
- package/validators/validate-helper-input-exist.js +98 -32
- package/validators/validate-model-attribute-exist.js +184 -52
- package/validators/validate-model-exist.js +14 -0
- package/validators/validate-required-helper-input.js +100 -39
package/SailsParser.js
CHANGED
|
@@ -60,30 +60,68 @@ class SailsParser {
|
|
|
60
60
|
const actionsRoot = path.join(this.rootDir, 'api', 'controllers')
|
|
61
61
|
const content = await this.#readFile(routesPath)
|
|
62
62
|
const routes = {}
|
|
63
|
+
const actionsToParse = []
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
try {
|
|
66
|
+
const ast = acorn.parse(content, {
|
|
67
|
+
ecmaVersion: 'latest',
|
|
68
|
+
sourceType: 'module'
|
|
69
|
+
})
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
71
|
+
walk.simple(ast, {
|
|
72
|
+
Property(node) {
|
|
73
|
+
const routePattern = node.key?.value || node.key?.name
|
|
74
|
+
if (!routePattern) return
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
let actionName = null
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
if (
|
|
79
|
+
node.value?.type === 'Literal' &&
|
|
80
|
+
typeof node.value.value === 'string'
|
|
81
|
+
) {
|
|
82
|
+
actionName = node.value.value
|
|
83
|
+
} else if (
|
|
84
|
+
node.value?.type === 'ObjectExpression' &&
|
|
85
|
+
node.value.properties
|
|
86
|
+
) {
|
|
87
|
+
for (const prop of node.value.properties) {
|
|
88
|
+
if (
|
|
89
|
+
prop.type === 'Property' &&
|
|
90
|
+
(prop.key?.name === 'action' || prop.key?.value === 'action') &&
|
|
91
|
+
prop.value?.type === 'Literal' &&
|
|
92
|
+
typeof prop.value.value === 'string'
|
|
93
|
+
) {
|
|
94
|
+
actionName = prop.value.value
|
|
95
|
+
break
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
79
99
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
100
|
+
if (
|
|
101
|
+
actionName &&
|
|
102
|
+
!actionName.startsWith('/') &&
|
|
103
|
+
!actionName.includes('://')
|
|
104
|
+
) {
|
|
105
|
+
actionsToParse.push({ routePattern, actionName })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
for (const { routePattern, actionName } of actionsToParse) {
|
|
111
|
+
const filePath =
|
|
112
|
+
path.join(actionsRoot, ...actionName.split('/')) + '.js'
|
|
113
|
+
const actionInfo = await this.#parseAction(filePath)
|
|
114
|
+
|
|
115
|
+
routes[routePattern] = {
|
|
116
|
+
action: {
|
|
117
|
+
name: actionName,
|
|
118
|
+
path: filePath,
|
|
119
|
+
...actionInfo
|
|
120
|
+
}
|
|
85
121
|
}
|
|
86
122
|
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('Error parsing routes:', error)
|
|
87
125
|
}
|
|
88
126
|
|
|
89
127
|
return routes
|
|
@@ -191,7 +229,11 @@ class SailsParser {
|
|
|
191
229
|
prop.key?.name === 'attributes' &&
|
|
192
230
|
prop.value?.type === 'ObjectExpression'
|
|
193
231
|
) {
|
|
194
|
-
defaultAttributes = context.#extractObjectLiteral(
|
|
232
|
+
defaultAttributes = context.#extractObjectLiteral(
|
|
233
|
+
prop.value,
|
|
234
|
+
true,
|
|
235
|
+
configCode
|
|
236
|
+
)
|
|
195
237
|
}
|
|
196
238
|
if (
|
|
197
239
|
prop.key?.name === 'models' &&
|
|
@@ -203,7 +245,9 @@ class SailsParser {
|
|
|
203
245
|
inner.value?.type === 'ObjectExpression'
|
|
204
246
|
) {
|
|
205
247
|
defaultAttributes = context.#extractObjectLiteral(
|
|
206
|
-
inner.value
|
|
248
|
+
inner.value,
|
|
249
|
+
true,
|
|
250
|
+
configCode
|
|
207
251
|
)
|
|
208
252
|
}
|
|
209
253
|
}
|
|
@@ -224,7 +268,11 @@ class SailsParser {
|
|
|
224
268
|
prop.key?.name === 'attributes' &&
|
|
225
269
|
prop.value?.type === 'ObjectExpression'
|
|
226
270
|
) {
|
|
227
|
-
defaultAttributes = context.#extractObjectLiteral(
|
|
271
|
+
defaultAttributes = context.#extractObjectLiteral(
|
|
272
|
+
prop.value,
|
|
273
|
+
true,
|
|
274
|
+
configCode
|
|
275
|
+
)
|
|
228
276
|
}
|
|
229
277
|
}
|
|
230
278
|
}
|
|
@@ -241,6 +289,7 @@ class SailsParser {
|
|
|
241
289
|
const name = file.slice(0, -3)
|
|
242
290
|
const modelPath = path.join(dir, file)
|
|
243
291
|
let attributes = {}
|
|
292
|
+
let attributesLine = 0
|
|
244
293
|
const context = this
|
|
245
294
|
|
|
246
295
|
try {
|
|
@@ -264,7 +313,12 @@ class SailsParser {
|
|
|
264
313
|
prop.key?.name === 'attributes' &&
|
|
265
314
|
prop.value?.type === 'ObjectExpression'
|
|
266
315
|
) {
|
|
267
|
-
attributes = context.#extractObjectLiteral(
|
|
316
|
+
attributes = context.#extractObjectLiteral(
|
|
317
|
+
prop.value,
|
|
318
|
+
true,
|
|
319
|
+
code
|
|
320
|
+
)
|
|
321
|
+
attributesLine = context.#getLineNumber(prop.key.start, code)
|
|
268
322
|
}
|
|
269
323
|
}
|
|
270
324
|
}
|
|
@@ -277,7 +331,11 @@ class SailsParser {
|
|
|
277
331
|
node.left.property.name === 'attributes' &&
|
|
278
332
|
node.right.type === 'ObjectExpression'
|
|
279
333
|
) {
|
|
280
|
-
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
|
+
)
|
|
281
339
|
}
|
|
282
340
|
},
|
|
283
341
|
ExportDefaultDeclaration(node) {
|
|
@@ -287,7 +345,12 @@ class SailsParser {
|
|
|
287
345
|
prop.key?.name === 'attributes' &&
|
|
288
346
|
prop.value?.type === 'ObjectExpression'
|
|
289
347
|
) {
|
|
290
|
-
attributes = context.#extractObjectLiteral(
|
|
348
|
+
attributes = context.#extractObjectLiteral(
|
|
349
|
+
prop.value,
|
|
350
|
+
true,
|
|
351
|
+
code
|
|
352
|
+
)
|
|
353
|
+
attributesLine = context.#getLineNumber(prop.key.start, code)
|
|
291
354
|
}
|
|
292
355
|
}
|
|
293
356
|
}
|
|
@@ -301,13 +364,22 @@ class SailsParser {
|
|
|
301
364
|
// Merge defaultAttributes first, then model attributes (model overrides default)
|
|
302
365
|
const mergedAttributes = {}
|
|
303
366
|
for (const key of Object.keys(defaultAttributes)) {
|
|
304
|
-
|
|
367
|
+
const attr = defaultAttributes[key]
|
|
368
|
+
mergedAttributes[key] = {
|
|
369
|
+
...attr,
|
|
370
|
+
path: modelsConfigPath
|
|
371
|
+
}
|
|
305
372
|
}
|
|
306
373
|
for (const key of Object.keys(attributes)) {
|
|
307
|
-
|
|
374
|
+
const attr = attributes[key]
|
|
375
|
+
mergedAttributes[key] = {
|
|
376
|
+
...attr,
|
|
377
|
+
path: modelPath
|
|
378
|
+
}
|
|
308
379
|
}
|
|
309
380
|
models[name] = {
|
|
310
381
|
path: modelPath,
|
|
382
|
+
attributesLine,
|
|
311
383
|
methods: STATIC_METHODS,
|
|
312
384
|
chainableMethods: CHAINABLE_METHODS,
|
|
313
385
|
attributes: mergedAttributes
|
|
@@ -444,7 +516,11 @@ class SailsParser {
|
|
|
444
516
|
prop.key.name === 'inputs' &&
|
|
445
517
|
prop.value.type === 'ObjectExpression'
|
|
446
518
|
) {
|
|
447
|
-
inputs = context.#extractObjectLiteral(
|
|
519
|
+
inputs = context.#extractObjectLiteral(
|
|
520
|
+
prop.value,
|
|
521
|
+
true,
|
|
522
|
+
content
|
|
523
|
+
)
|
|
448
524
|
}
|
|
449
525
|
if (
|
|
450
526
|
prop.key &&
|
|
@@ -475,7 +551,11 @@ class SailsParser {
|
|
|
475
551
|
prop.key.name === 'inputs' &&
|
|
476
552
|
prop.value.type === 'ObjectExpression'
|
|
477
553
|
) {
|
|
478
|
-
inputs = context.#extractObjectLiteral(
|
|
554
|
+
inputs = context.#extractObjectLiteral(
|
|
555
|
+
prop.value,
|
|
556
|
+
true,
|
|
557
|
+
content
|
|
558
|
+
)
|
|
479
559
|
}
|
|
480
560
|
if (
|
|
481
561
|
prop.key &&
|
|
@@ -501,14 +581,13 @@ class SailsParser {
|
|
|
501
581
|
const match = content.match(/inputs\s*:\s*\{([\s\S]*?)\n\s*\}/m)
|
|
502
582
|
if (match) {
|
|
503
583
|
try {
|
|
504
|
-
// Try to parse as JS object
|
|
505
584
|
const fakeObj = `({${match[1]}})`
|
|
506
585
|
const ast = acorn.parse(fakeObj, { ecmaVersion: 'latest' })
|
|
507
586
|
let obj = {}
|
|
508
587
|
walk.simple(ast, {
|
|
509
588
|
ObjectExpression(node) {
|
|
510
589
|
if (!obj || Object.keys(obj).length === 0) {
|
|
511
|
-
obj = context.#extractObjectLiteral(node)
|
|
590
|
+
obj = context.#extractObjectLiteral(node, true, content)
|
|
512
591
|
}
|
|
513
592
|
}
|
|
514
593
|
})
|
|
@@ -536,7 +615,7 @@ class SailsParser {
|
|
|
536
615
|
return helpers
|
|
537
616
|
}
|
|
538
617
|
|
|
539
|
-
#extractObjectLiteral(node) {
|
|
618
|
+
#extractObjectLiteral(node, withLineNumbers = false, sourceCode = null) {
|
|
540
619
|
if (node.type !== 'ObjectExpression') return undefined
|
|
541
620
|
const obj = {}
|
|
542
621
|
for (const prop of node.properties) {
|
|
@@ -545,11 +624,15 @@ class SailsParser {
|
|
|
545
624
|
prop.key.type === 'Identifier' ? prop.key.name : prop.key.value
|
|
546
625
|
let value
|
|
547
626
|
if (prop.value.type === 'ObjectExpression') {
|
|
548
|
-
value = this.#extractObjectLiteral(
|
|
627
|
+
value = this.#extractObjectLiteral(
|
|
628
|
+
prop.value,
|
|
629
|
+
withLineNumbers,
|
|
630
|
+
sourceCode
|
|
631
|
+
)
|
|
549
632
|
} else if (prop.value.type === 'ArrayExpression') {
|
|
550
633
|
value = prop.value.elements.map((el) =>
|
|
551
634
|
el.type === 'ObjectExpression'
|
|
552
|
-
? this.#extractObjectLiteral(el)
|
|
635
|
+
? this.#extractObjectLiteral(el, withLineNumbers, sourceCode)
|
|
553
636
|
: el.type === 'Literal'
|
|
554
637
|
? el.value
|
|
555
638
|
: el.type === 'Identifier'
|
|
@@ -563,11 +646,20 @@ class SailsParser {
|
|
|
563
646
|
} else {
|
|
564
647
|
value = undefined
|
|
565
648
|
}
|
|
566
|
-
|
|
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
|
+
}
|
|
567
655
|
}
|
|
568
656
|
}
|
|
569
657
|
return obj
|
|
570
658
|
}
|
|
659
|
+
#getLineNumber(offset, sourceCode) {
|
|
660
|
+
const lines = sourceCode.substring(0, offset).split('\n')
|
|
661
|
+
return lines.length
|
|
662
|
+
}
|
|
571
663
|
#getDataTypes() {
|
|
572
664
|
return [
|
|
573
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,
|
|
@@ -21,30 +21,31 @@ module.exports = function modelAttributePropsCompletion(
|
|
|
21
21
|
const insideAttributes = /attributes\s*:\s*{([\s\S]*)$/.exec(before)
|
|
22
22
|
if (!insideAttributes) return []
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
let
|
|
27
|
-
|
|
24
|
+
// Count nesting depth from attributes: { to current position
|
|
25
|
+
// We want depth > 1 (inside a property) not depth === 1 (top level of attributes)
|
|
26
|
+
let depth = 0
|
|
27
|
+
let foundAttributesBlock = false
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < lines.length; i++) {
|
|
28
30
|
const line = lines[i]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
31
|
+
|
|
32
|
+
if (/attributes\s*:\s*{/.test(line)) {
|
|
33
|
+
foundAttributesBlock = true
|
|
34
|
+
depth = 1
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (foundAttributesBlock) {
|
|
39
|
+
for (let j = 0; j < line.length; j++) {
|
|
40
|
+
if (line[j] === '{') depth++
|
|
41
|
+
if (line[j] === '}') depth--
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
|
-
if (insideProperty) break
|
|
44
|
-
if (/^\s*attributes\s*:\s*{/.test(line)) break
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
if (
|
|
46
|
+
// Only provide completions if we're nested inside a property (depth > 1)
|
|
47
|
+
// depth === 1 means we're at the top level of attributes: {}
|
|
48
|
+
if (depth <= 1) return []
|
|
48
49
|
|
|
49
50
|
return typeMap.modelAttributeProps.map(({ label, detail }) => ({
|
|
50
51
|
label,
|