@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 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
- const regex = /['"]([^'"]+)['"]\s*:\s*['"]([^'"]+)['"]/g
65
- let match
66
- while ((match = regex.exec(content))) {
67
- const route = match[1]
68
- const actionName = match[2]
65
+ try {
66
+ const ast = acorn.parse(content, {
67
+ ecmaVersion: 'latest',
68
+ sourceType: 'module'
69
+ })
69
70
 
70
- // Skip redirects and external URLs
71
- // Routes that start with '/' or contain '://' are redirects, not actions
72
- if (actionName.startsWith('/') || actionName.includes('://')) {
73
- continue
74
- }
71
+ walk.simple(ast, {
72
+ Property(node) {
73
+ const routePattern = node.key?.value || node.key?.name
74
+ if (!routePattern) return
75
75
 
76
- const filePath = path.join(actionsRoot, ...actionName.split('/')) + '.js'
76
+ let actionName = null
77
77
 
78
- const actionInfo = await this.#parseAction(filePath)
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
- routes[route] = {
81
- action: {
82
- name: actionName,
83
- path: filePath,
84
- ...actionInfo
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(prop.value)
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(prop.value)
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(prop.value)
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(prop.value)
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
- mergedAttributes[key] = defaultAttributes[key]
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
- mergedAttributes[key] = attributes[key]
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(prop.value)
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(prop.value)
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(prop.value)
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
- obj[key] = value
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
- let type = inputDef?.type
17
- let required = inputDef?.required ? 'required' : 'optional'
18
- let description = inputDef?.description || ''
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
- // Use a stack to track braces and find if we're inside a property block
25
- let braceStack = []
26
- let insideProperty = false
27
- for (let i = lines.length - 1; i >= 0; i--) {
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
- for (let j = line.length - 1; j >= 0; j--) {
30
- if (line[j] === '}') braceStack.push('}')
31
- if (line[j] === '{') {
32
- if (braceStack.length > 0) {
33
- braceStack.pop()
34
- } else {
35
- const propMatch = lines[i]
36
- .slice(0, j + 1)
37
- .match(/([a-zA-Z0-9_]+)\s*:\s*{$/)
38
- if (propMatch) insideProperty = true
39
- break
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 (!insideProperty) return []
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,