@sailshq/language-server 0.2.2 → 0.3.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.
@@ -18,18 +18,12 @@ module.exports = function modelAttributesCompletion(
18
18
  const criteriaMatch = before.match(
19
19
  /(?:sails\.models\.([A-Za-z_$][\w$]*)|([A-Za-z_$][\w$]*))\s*\.\w+\s*\(\s*\{[^}]*([a-zA-Z0-9_]*)?$/
20
20
  )
21
- const selectStringMatch = before.match(
22
- /(?:select|omit|sort)\s*:\s*['"]([a-zA-Z0-9_]*)?$/
23
- )
24
- const selectArrayMatch = before.match(
21
+ const sortStringMatch = before.match(/sort\s*:\s*['"]([a-zA-Z0-9_]*)?$/)
22
+ const criteriaOptionsArrayMatch = before.match(
25
23
  /(?:select|omit|sort)\s*:\s*\[\s*['"]([a-zA-Z0-9_]*)?$/
26
24
  )
27
- const populateStringMatch = before.match(
28
- /populate\s*:\s*['"]([a-zA-Z0-9_]*)?$/
29
- )
30
- const sortStringMatch = before.match(/sort\s*:\s*['"]([a-zA-Z0-9_]*)?$/)
31
- const sortArrayStringMatch = before.match(
32
- /sort\s*:\s*\[\s*[^\{\]]*['"]([a-zA-Z0-9_]*)?$/
25
+ const popuplateMethodMatch = before.match(
26
+ /\.populate\s*\(\s*['"]([a-zA-Z0-9_]*)?$/
33
27
  )
34
28
  const sortArrayObjectMatch = before.match(
35
29
  /sort\s*:\s*\[\s*\{\s*([a-zA-Z0-9_]*)?$/
@@ -44,7 +38,7 @@ module.exports = function modelAttributesCompletion(
44
38
  // Detect if we are inside a .select([]), .omit([]), .sort([]), etc. as a method call (e.g. User.find().select([]))
45
39
  // This matches e.g. .select(['foo', '']) or .omit(["bar", '']) or .select('foo')
46
40
  const chainableDirectCallMatch = before.match(
47
- /\.(select|omit|sort|populate|where)\s*\(\s*\[?\s*['"]?([a-zA-Z0-9_]*)?$/
41
+ /\.(select|omit|sort|populate|where)\s*\(\s*\[.*(?:,|\[)?\s*['"`]([a-zA-Z0-9_]*)?$/
48
42
  )
49
43
 
50
44
  // Also allow completions in .where({ ... }) chainable method call context
@@ -56,41 +50,52 @@ module.exports = function modelAttributesCompletion(
56
50
  /([A-Za-z_$][\w$]*)\s*\.where\s*\(\s*\{[^}]*([a-zA-Z0-9_]*)?$/
57
51
  )
58
52
 
53
+ // Support chained .where({ ... }) completions ---
54
+ // Try to infer model name from chained calls like User.find().where({ ... })
55
+ const chainedWhereMatch = before.match(
56
+ /([A-Za-z_$][\w$]*)\s*\.[\w$]+\s*\(.*?\)\s*\.where\s*\(\s*\{[^}]*([a-zA-Z0-9_]*)?$/
57
+ )
58
+
59
59
  // Only suppress completions after a colon (:) in object literals for static methods,
60
60
  // but always allow completions in .select(['']), .omit(['']), .sort(['']), .where({}), etc.
61
61
  const inChainableString =
62
- selectStringMatch ||
63
- selectArrayMatch ||
62
+ criteriaOptionsArrayMatch ||
64
63
  sortStringMatch ||
65
- sortArrayStringMatch ||
66
64
  sortArrayObjectMatch ||
67
- populateStringMatch ||
65
+ popuplateMethodMatch ||
68
66
  isInChainableMethodCall ||
69
67
  isInWhereMethodCall ||
70
68
  !!chainableDirectCallMatch
71
69
 
72
70
  // Suppress completions after a colon only if NOT in a chainable string/array context
73
- if (!inChainableString) {
74
- const lines = before.split('\n')
75
- const line = lines[lines.length - 1]
76
- const beforeCursor = line.slice(0, position.character)
77
- // If the last non-whitespace character before the cursor is a colon, suppress completion
78
- // (but allow after comma, or at start of line/object)
79
- const lastColon = beforeCursor.lastIndexOf(':')
80
- const lastComma = beforeCursor.lastIndexOf(',')
81
- if (lastColon > lastComma && lastColon > beforeCursor.lastIndexOf('{')) {
82
- // Check if we are inside a string (e.g. after a colon and inside quotes)
83
- // If so, suppress completion
84
- const quoteBefore = beforeCursor.lastIndexOf("'")
85
- const dquoteBefore = beforeCursor.lastIndexOf('"')
86
- if (
87
- (quoteBefore > lastColon && quoteBefore > lastComma) ||
88
- (dquoteBefore > lastColon && dquoteBefore > lastComma)
89
- ) {
71
+ // Also suppress completions after colon in .where({ ... }) context, unless after a comma or at start
72
+ // FIX: Do not run this suppression logic at all if we are in a select/omit/sort array (object property form)
73
+ if (!criteriaOptionsArrayMatch) {
74
+ if (
75
+ !inChainableString ||
76
+ ((isInWhereMethodCall || chainedWhereMatch) && !criteriaOptionsArrayMatch)
77
+ ) {
78
+ const lines = before.split('\n')
79
+ const line = lines[lines.length - 1]
80
+ const beforeCursor = line.slice(0, position.character)
81
+ // If the last non-whitespace character before the cursor is a colon, suppress completion
82
+ // (but allow after comma, or at start of line/object)
83
+ const lastColon = beforeCursor.lastIndexOf(':')
84
+ const lastComma = beforeCursor.lastIndexOf(',')
85
+ if (lastColon > lastComma && lastColon > beforeCursor.lastIndexOf('{')) {
86
+ // Check if we are inside a string (e.g. after a colon and inside quotes)
87
+ // If so, suppress completion
88
+ const quoteBefore = beforeCursor.lastIndexOf("'")
89
+ const dquoteBefore = beforeCursor.lastIndexOf('"')
90
+ if (
91
+ (quoteBefore > lastColon && quoteBefore > lastComma) ||
92
+ (dquoteBefore > lastColon && dquoteBefore > lastComma)
93
+ ) {
94
+ return []
95
+ }
96
+ // Otherwise, suppress completion after colon
90
97
  return []
91
98
  }
92
- // Otherwise, suppress completion after colon
93
- return []
94
99
  }
95
100
  }
96
101
 
@@ -114,23 +119,35 @@ module.exports = function modelAttributesCompletion(
114
119
  return last[1] || last[2] || null
115
120
  }
116
121
 
122
+ // Determine the current Sails.js model context and attribute prefix for completions
123
+ // by matching the code before the cursor against various Sails.js query patterns.
124
+ // This enables context-aware attribute completions for all supported query forms.
117
125
  if (criteriaMatch) {
118
126
  modelName = criteriaMatch[1] || criteriaMatch[2]
119
127
  prefix = criteriaMatch[3] || ''
120
- } else if (selectStringMatch || selectArrayMatch) {
128
+ } else if (criteriaOptionsArrayMatch) {
121
129
  modelName = inferModelName(before)
122
- prefix = (selectStringMatch || selectArrayMatch)[1] || ''
123
- } else if (populateStringMatch) {
130
+ prefix = criteriaOptionsArrayMatch[1] || ''
131
+ } else if (popuplateMethodMatch) {
124
132
  isPopulate = true
125
133
  modelName = inferModelName(before)
126
- prefix = populateStringMatch[1] || ''
127
- } else if (sortStringMatch || sortArrayStringMatch || sortArrayObjectMatch) {
134
+ prefix = popuplateMethodMatch[1] || ''
135
+ } else if (
136
+ sortStringMatch ||
137
+ criteriaOptionsArrayMatch ||
138
+ sortArrayObjectMatch
139
+ ) {
128
140
  modelName = inferModelName(before)
129
141
  prefix =
130
- (sortStringMatch || sortArrayStringMatch || sortArrayObjectMatch)[1] || ''
142
+ (sortStringMatch ||
143
+ criteriaOptionsArrayMatch ||
144
+ sortArrayObjectMatch)[1] || ''
131
145
  } else if (whereMethodCallMatch) {
132
146
  modelName = whereMethodCallMatch[1]
133
147
  prefix = whereMethodCallMatch[2] || ''
148
+ } else if (chainedWhereMatch) {
149
+ modelName = chainedWhereMatch[1]
150
+ prefix = chainedWhereMatch[2] || ''
134
151
  } else if (chainableDirectCallMatch) {
135
152
  modelName = inferModelName(before)
136
153
  prefix = chainableDirectCallMatch[2] || ''
@@ -166,6 +183,59 @@ module.exports = function modelAttributesCompletion(
166
183
  }
167
184
  }
168
185
 
186
+ // Remove already-used attributes for both object and array/chainable forms
187
+ // Collect all used attributes from any select/omit/sort array in object or chainable form up to the cursor
188
+ const allArrayRegex =
189
+ /(select|omit|sort)\s*:\s*\[([^\]]*)\]|\.(select|omit|sort)\s*\(\s*\[([^\]]*)/g
190
+ let match
191
+ while ((match = allArrayRegex.exec(before)) !== null) {
192
+ const arrayContent = match[2] || match[4] || ''
193
+ const usedInArray = Array.from(
194
+ arrayContent.matchAll(/['"`]\s*([a-zA-Z0-9_]+)\s*['"`]/g)
195
+ ).map((m) => m[1])
196
+ usedInArray.forEach((attr) => usedProps.add(attr))
197
+ }
198
+
199
+ // Improved: Only trigger completions in object form select/omit/sort arrays when inside a string (between quotes)
200
+ if (criteriaOptionsArrayMatch) {
201
+ // Find the last '[' before the cursor
202
+ const arrayStart = before.lastIndexOf('[')
203
+ if (arrayStart !== -1) {
204
+ const arrayContent = before.slice(arrayStart, offset)
205
+ // Use the same logic as chainable: check for a quote before the cursor (inside a string)
206
+ const quoteMatch = arrayContent.match(/['"`]([^'"`]*)$/)
207
+ if (!quoteMatch) {
208
+ // Not inside a string, suppress completions
209
+ return []
210
+ }
211
+ // Also: filter out already-used attributes in this array
212
+ const usedInArray = Array.from(
213
+ arrayContent.matchAll(/['"`]\s*([a-zA-Z0-9_]+)\s*['"`]/g)
214
+ ).map((m) => m[1])
215
+ usedInArray.forEach((attr) => usedProps.add(attr))
216
+ }
217
+ }
218
+
219
+ if (chainableDirectCallMatch) {
220
+ // For array/chainable forms, parse the array up to the cursor and collect used attributes
221
+ const arrayMatch = before.match(/\[([^\]]*)$/)
222
+ if (arrayMatch) {
223
+ const arrayContent = arrayMatch[1]
224
+ // Fix: allow completions for any string in the array, not just the first
225
+ // Find the last quote and ensure the cursor is after it (inside a string)
226
+ const quoteMatch = arrayContent.match(/['"`][^'"`]*$/)
227
+ if (!quoteMatch) {
228
+ // Not inside a string, suppress completions
229
+ return []
230
+ }
231
+ // Also: filter out already-used attributes in this array
232
+ const usedInArray = Array.from(
233
+ arrayContent.matchAll(/['"`]\s*([a-zA-Z0-9_]+)\s*['"`]/g)
234
+ ).map((m) => m[1])
235
+ usedInArray.forEach((attr) => usedProps.add(attr))
236
+ }
237
+ }
238
+
169
239
  if (isPopulate) {
170
240
  attributes = Object.entries(model.attributes || {})
171
241
  .filter(([, def]) => def && (def.model || def.collection))
@@ -174,22 +244,25 @@ module.exports = function modelAttributesCompletion(
174
244
  attributes = Object.keys(model.attributes || {})
175
245
  }
176
246
 
177
- return attributes
178
- .filter((attr) => attr.startsWith(prefix))
179
- .filter((attr) => !usedProps.has(attr))
180
- .map((attr) => {
181
- const attrDef = model.attributes && model.attributes[attr]
182
- let type = attrDef && attrDef.type ? attrDef.type : ''
183
- let required = attrDef && attrDef.required ? 'required' : 'optional'
184
- let detail = type ? `${type} (${required})` : required
185
- return {
186
- label: attr,
187
- kind: lsp.CompletionItemKind.Field,
188
- detail,
189
- documentation: `${modelName}.${attr}`,
190
- sortText: attr,
191
- filterText: attr,
192
- insertText: attr
193
- }
194
- })
247
+ return Array.from(
248
+ new Set(
249
+ attributes
250
+ .filter((attr) => attr.toLowerCase().startsWith(prefix.toLowerCase()))
251
+ .filter((attr) => !usedProps.has(attr))
252
+ )
253
+ ).map((attr) => {
254
+ const attrDef = model.attributes && model.attributes[attr]
255
+ let type = attrDef && attrDef.type ? attrDef.type : ''
256
+ let required = attrDef && attrDef.required ? 'required' : 'optional'
257
+ let detail = type ? `${type} (${required})` : required
258
+ return {
259
+ label: attr,
260
+ kind: lsp.CompletionItemKind.Field,
261
+ detail,
262
+ documentation: `${modelName}.${attr}`,
263
+ sortText: attr,
264
+ filterText: attr,
265
+ insertText: attr
266
+ }
267
+ })
195
268
  }
@@ -15,10 +15,10 @@ module.exports = function modelMethodsCompletion(document, position, typeMap) {
15
15
  const staticCallMatch = before.match(
16
16
  /(?:sails\.models\.([A-Za-z_$][\w$]*)|([A-Za-z_$][\w$]*))\.\s*([a-zA-Z]*)?$/
17
17
  )
18
- // Match chainable calls like User.find().<chainable> or User.find({...}).<chainable>
19
- const chainableCallMatch = before.match(
20
- /([A-Za-z_$][\w$]*)\.[a-zA-Z_]+\([^)]*\)\.\s*([a-zA-Z]*)?$/
21
- )
18
+ // Match all chainable calls and get the last one for completion
19
+ const chainableCallMatches = [
20
+ ...before.matchAll(/([A-Za-z_$][\w$]*)\.[a-zA-Z_]+\([^)]*\)/g)
21
+ ]
22
22
 
23
23
  let modelName, prefix, methods
24
24
 
@@ -26,11 +26,15 @@ module.exports = function modelMethodsCompletion(document, position, typeMap) {
26
26
  const models = typeMap.models || {}
27
27
  const modelKeys = Object.keys(models)
28
28
 
29
- if (chainableCallMatch) {
30
- modelName = chainableCallMatch[1]
31
- prefix = chainableCallMatch[2] || ''
32
- if (!modelName) return []
33
- const foundKey = modelKeys.find(
29
+ if (chainableCallMatches.length > 0) {
30
+ // Use the last chainable call in the chain
31
+ const lastMatch = chainableCallMatches[chainableCallMatches.length - 1]
32
+ modelName = lastMatch[1]
33
+ // Get the prefix after the last dot (if user is typing e.g. .select)
34
+ const afterLastChain = before.slice(lastMatch.index + lastMatch[0].length)
35
+ const prefixMatch = afterLastChain.match(/\.\s*([a-zA-Z]*)?$/)
36
+ prefix = (prefixMatch && prefixMatch[1]) || ''
37
+ const foundKey = Object.keys(models).find(
34
38
  (k) => k.toLowerCase() === modelName.toLowerCase()
35
39
  )
36
40
  methods = foundKey ? models[foundKey].chainableMethods || [] : []
@@ -52,7 +56,7 @@ module.exports = function modelMethodsCompletion(document, position, typeMap) {
52
56
  let insertText = method.name + '($0)'
53
57
  // For chainable .select or .omit, insert ([''])
54
58
  if (
55
- chainableCallMatch &&
59
+ chainableCallMatches.length > 0 &&
56
60
  (method.name === 'select' || method.name === 'omit')
57
61
  ) {
58
62
  insertText = method.name + '([$0])'
@@ -3,7 +3,7 @@ const path = require('path')
3
3
 
4
4
  module.exports = async function goToAction(document, position, typeMap) {
5
5
  const fileName = path.basename(document.uri)
6
- if (fileName !== 'routes.js') return null
6
+ if (fileName !== 'routes.js') return []
7
7
 
8
8
  const text = document.getText()
9
9
  const offset = document.offsetAt(position)
@@ -39,5 +39,5 @@ module.exports = async function goToAction(document, position, typeMap) {
39
39
  }
40
40
  }
41
41
  }
42
- return null
42
+ return []
43
43
  }
package/index.js CHANGED
@@ -101,7 +101,7 @@ connection.onDefinition(async (params) => {
101
101
  helperDefinition,
102
102
  modelDefinition
103
103
  ].filter(Boolean)
104
- return definitions.length > 0 ? definitions : null
104
+ return definitions
105
105
  })
106
106
 
107
107
  connection.onCompletion(async (params) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailshq/language-server",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "Language Server Protocol server for Sails Language Service",
5
5
  "homepage": "https://sailjs.com",
6
6
  "repository": {
@@ -34,167 +34,422 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
34
34
  })
35
35
  walk.simple(ast, {
36
36
  CallExpression(node) {
37
- if (
38
- node.callee &&
39
- node.callee.type === 'MemberExpression' &&
40
- node.arguments &&
41
- node.arguments.length > 0
42
- ) {
37
+ if (node.callee && node.callee.type === 'MemberExpression') {
43
38
  const method = node.callee.property.name
44
- const modelName = node.callee.object.name
45
- // Only check for Waterline methods
39
+ // --- Robust model name extraction for ALL method calls (including chainable) ---
40
+ let effectiveModelName = undefined
41
+ let obj = node.callee.object
42
+ while (obj) {
43
+ if (obj.type === 'Identifier') {
44
+ effectiveModelName = obj.name
45
+ break
46
+ } else if (
47
+ obj.type === 'CallExpression' &&
48
+ obj.callee &&
49
+ obj.callee.type === 'MemberExpression'
50
+ ) {
51
+ obj = obj.callee.object
52
+ } else {
53
+ break
54
+ }
55
+ }
56
+ const model = getModelByName(effectiveModelName)
57
+ if (!model) return
58
+
59
+ // Only validate chainable methods that are select, omit, sort, or populate
60
+ const allowedChainable = ['select', 'omit', 'sort', 'populate']
61
+
62
+ // --- Ignore validation for arguments to non-model chainable methods like .intercept ---
63
+ // If the current method is NOT a model method or allowedChainable, skip validation for its arguments
64
+ const modelMethods = [
65
+ 'create',
66
+ 'createEach',
67
+ 'count',
68
+ 'find',
69
+ 'findOne',
70
+ 'update',
71
+ 'destroy',
72
+ 'where',
73
+ 'findOrCreate',
74
+ 'sum',
75
+ ...allowedChainable
76
+ ]
77
+ if (!modelMethods.includes(method)) {
78
+ // This is a non-model method (e.g., intercept, using, etc.), skip validation for its arguments
79
+ return
80
+ }
81
+
82
+ // handle createEach array of objects
46
83
  if (
47
- [
48
- 'create',
49
- 'createEach',
50
- 'count',
51
- 'find',
52
- 'findOne',
53
- 'update',
54
- 'destroy',
55
- 'where',
56
- 'findOrCreate',
57
- 'sum'
58
- ].includes(method)
84
+ method === 'createEach' &&
85
+ node.arguments[0] &&
86
+ node.arguments[0].type === 'ArrayExpression'
59
87
  ) {
60
- const model = getModelByName(modelName)
61
- if (!model) return
62
- // --- FIX: handle createEach array of objects ---
63
- if (
64
- method === 'createEach' &&
65
- node.arguments[0].type === 'ArrayExpression'
66
- ) {
67
- for (const el of node.arguments[0].elements) {
68
- if (!el || el.type !== 'ObjectExpression') continue
69
- for (const prop of el.properties) {
70
- const attribute =
71
- prop.key && (prop.key.name || prop.key.value)
88
+ for (const el of node.arguments[0].elements) {
89
+ if (!el || el.type !== 'ObjectExpression') continue
90
+ for (const prop of el.properties) {
91
+ const attribute = prop.key && (prop.key.name || prop.key.value)
92
+ if (
93
+ !model.attributes ||
94
+ !Object.prototype.hasOwnProperty.call(
95
+ model.attributes,
96
+ attribute
97
+ )
98
+ ) {
99
+ diagnostics.push(
100
+ lsp.Diagnostic.create(
101
+ lsp.Range.create(
102
+ document.positionAt(prop.key.start),
103
+ document.positionAt(prop.key.end)
104
+ ),
105
+ `'${attribute}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
106
+ lsp.DiagnosticSeverity.Error,
107
+ 'sails-lsp'
108
+ )
109
+ )
110
+ }
111
+ }
112
+ }
113
+ return
114
+ }
115
+
116
+ // Validate attributes in chained .where({ ... }) calls
117
+ if (
118
+ method === 'where' &&
119
+ node.arguments[0] &&
120
+ node.arguments[0].type === 'ObjectExpression'
121
+ ) {
122
+ for (const prop of node.arguments[0].properties) {
123
+ if (!prop.key) continue
124
+ const whereAttr = prop.key.name || prop.key.value
125
+ if (
126
+ !model.attributes ||
127
+ !Object.prototype.hasOwnProperty.call(
128
+ model.attributes,
129
+ whereAttr
130
+ )
131
+ ) {
132
+ diagnostics.push(
133
+ lsp.Diagnostic.create(
134
+ lsp.Range.create(
135
+ document.positionAt(prop.key.start),
136
+ document.positionAt(prop.key.end)
137
+ ),
138
+ `'${whereAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
139
+ lsp.DiagnosticSeverity.Error,
140
+ 'sails-lsp'
141
+ )
142
+ )
143
+ }
144
+ }
145
+ return
146
+ }
147
+
148
+ // --- Unified validation for .select([]), .omit([]), .sort([]), .populate([]) chainable calls only ---
149
+ if (
150
+ allowedChainable.includes(method) &&
151
+ node.arguments[0] &&
152
+ node.arguments[0].type === 'ArrayExpression'
153
+ ) {
154
+ for (const el of node.arguments[0].elements) {
155
+ if (!el) continue
156
+ let arrAttr = undefined
157
+ if (el.type === 'Literal' || el.type === 'StringLiteral') {
158
+ arrAttr = el.value
159
+ } else if (
160
+ el.type === 'TemplateLiteral' &&
161
+ el.expressions.length === 0
162
+ ) {
163
+ arrAttr = el.quasis[0].value.cooked
164
+ } else if (el.type === 'ObjectExpression' && method === 'sort') {
165
+ // For sort([{ foo: 1 }])
166
+ for (const sortProp of el.properties) {
167
+ if (!sortProp.key) continue
168
+ const sortAttr = sortProp.key.name || sortProp.key.value
72
169
  if (
73
170
  !model.attributes ||
74
171
  !Object.prototype.hasOwnProperty.call(
75
172
  model.attributes,
76
- attribute
173
+ sortAttr
77
174
  )
78
175
  ) {
79
176
  diagnostics.push(
80
177
  lsp.Diagnostic.create(
81
178
  lsp.Range.create(
82
- document.positionAt(prop.key.start),
83
- document.positionAt(prop.key.end)
179
+ document.positionAt(sortProp.key.start),
180
+ document.positionAt(sortProp.key.end)
84
181
  ),
85
- `'${attribute}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
182
+ `'${sortAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
86
183
  lsp.DiagnosticSeverity.Error,
87
184
  'sails-lsp'
88
185
  )
89
186
  )
90
187
  }
91
188
  }
189
+ continue
190
+ }
191
+ if (arrAttr !== undefined) {
192
+ // For sort, allow 'foo ASC' or 'foo DESC'
193
+ let checkAttr = arrAttr
194
+ if (method === 'sort' && typeof arrAttr === 'string') {
195
+ checkAttr = arrAttr.split(' ')[0]
196
+ }
197
+ if (
198
+ !checkAttr ||
199
+ typeof checkAttr !== 'string' ||
200
+ checkAttr.trim() === ''
201
+ ) {
202
+ diagnostics.push(
203
+ lsp.Diagnostic.create(
204
+ lsp.Range.create(
205
+ document.positionAt(el.start),
206
+ document.positionAt(el.end)
207
+ ),
208
+ `Empty or invalid attribute in .${method}() for model '${effectiveModelName}'.`,
209
+ lsp.DiagnosticSeverity.Error,
210
+ 'sails-lsp'
211
+ )
212
+ )
213
+ continue
214
+ }
215
+ if (
216
+ !model.attributes ||
217
+ !Object.prototype.hasOwnProperty.call(
218
+ model.attributes,
219
+ checkAttr
220
+ )
221
+ ) {
222
+ diagnostics.push(
223
+ lsp.Diagnostic.create(
224
+ lsp.Range.create(
225
+ document.positionAt(el.start),
226
+ document.positionAt(el.end)
227
+ ),
228
+ `'${checkAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
229
+ lsp.DiagnosticSeverity.Error,
230
+ 'sails-lsp'
231
+ )
232
+ )
233
+ }
92
234
  }
93
- return
94
235
  }
95
- // --- END FIX ---
96
- if (node.arguments[0].type === 'ObjectExpression') {
97
- for (const prop of node.arguments[0].properties) {
98
- const attribute = prop.key && (prop.key.name || prop.key.value)
99
- const queryOptionKeys = [
100
- 'where',
101
- 'select',
102
- 'omit',
103
- 'sort',
104
- 'limit',
105
- 'skip',
106
- 'page',
107
- 'populate',
108
- 'groupBy',
109
- 'having',
110
- 'sum',
111
- 'average',
112
- 'min',
113
- 'max',
114
- 'distinct',
115
- 'meta'
116
- ]
117
- // For non-create methods, validate all top-level keys except query option keys
236
+ // Do NOT return here; allow walker to continue to chained calls
237
+ }
238
+
239
+ // Validate attributes in .find({ ... }) and similar methods
240
+ if (
241
+ node.arguments[0] &&
242
+ node.arguments[0].type === 'ObjectExpression'
243
+ ) {
244
+ // Only validate if this is a top-level model method call, not an argument to another method (e.g., intercept)
245
+ // Check that the callee is a direct property of an Identifier (the model), not a nested CallExpression
246
+ let isTopLevelModelCall = false
247
+ let calleeObj = node.callee.object
248
+ if (calleeObj && calleeObj.type === 'Identifier') {
249
+ isTopLevelModelCall = true
250
+ } else if (calleeObj && calleeObj.type === 'CallExpression') {
251
+ // If the parent is a CallExpression, but the root is an Identifier, still allow
252
+ let root = calleeObj
253
+ while (
254
+ root &&
255
+ root.type === 'CallExpression' &&
256
+ root.callee &&
257
+ root.callee.type === 'MemberExpression'
258
+ ) {
259
+ root = root.callee.object
260
+ }
261
+ if (root && root.type === 'Identifier') {
262
+ isTopLevelModelCall = true
263
+ }
264
+ }
265
+ // If this is an argument to a non-model method (e.g., intercept), skip validation
266
+ if (!isTopLevelModelCall) return
267
+ for (const prop of node.arguments[0].properties) {
268
+ const attribute = prop.key && (prop.key.name || prop.key.value)
269
+ const queryOptionKeys = [
270
+ 'where',
271
+ 'select',
272
+ 'omit',
273
+ 'sort',
274
+ 'limit',
275
+ 'skip',
276
+ 'page',
277
+ 'populate',
278
+ 'groupBy',
279
+ 'having',
280
+ 'sum',
281
+ 'average',
282
+ 'min',
283
+ 'max',
284
+ 'distinct',
285
+ 'meta',
286
+ 'or',
287
+ 'and',
288
+ 'not'
289
+ ]
290
+ // For non-create methods, validate all top-level keys except query option keys
291
+ if (
292
+ method !== 'create' &&
293
+ method !== 'createEach' &&
294
+ !queryOptionKeys.includes(attribute)
295
+ ) {
118
296
  if (
119
- method !== 'create' &&
120
- method !== 'createEach' &&
121
- !queryOptionKeys.includes(attribute)
297
+ !model.attributes ||
298
+ !Object.prototype.hasOwnProperty.call(
299
+ model.attributes,
300
+ attribute
301
+ )
122
302
  ) {
123
- if (
124
- !model.attributes ||
125
- !Object.prototype.hasOwnProperty.call(
126
- model.attributes,
127
- attribute
303
+ diagnostics.push(
304
+ lsp.Diagnostic.create(
305
+ lsp.Range.create(
306
+ document.positionAt(prop.key.start),
307
+ document.positionAt(prop.key.end)
308
+ ),
309
+ `'${attribute}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
310
+ lsp.DiagnosticSeverity.Error,
311
+ 'sails-lsp'
128
312
  )
129
- ) {
130
- diagnostics.push(
131
- lsp.Diagnostic.create(
132
- lsp.Range.create(
133
- document.positionAt(prop.key.start),
134
- document.positionAt(prop.key.end)
135
- ),
136
- `'${attribute}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
137
- lsp.DiagnosticSeverity.Error,
138
- 'sails-lsp'
313
+ )
314
+ }
315
+ continue
316
+ }
317
+ if (
318
+ method !== 'create' &&
319
+ method !== 'createEach' &&
320
+ queryOptionKeys.includes(attribute)
321
+ ) {
322
+ if (
323
+ attribute === 'where' &&
324
+ prop.value &&
325
+ prop.value.type === 'ObjectExpression'
326
+ ) {
327
+ for (const whereProp of prop.value.properties) {
328
+ if (!whereProp.key) continue
329
+ const whereAttr = whereProp.key.name || whereProp.key.value
330
+ if (
331
+ !model.attributes ||
332
+ !Object.prototype.hasOwnProperty.call(
333
+ model.attributes,
334
+ whereAttr
139
335
  )
140
- )
336
+ ) {
337
+ diagnostics.push(
338
+ lsp.Diagnostic.create(
339
+ lsp.Range.create(
340
+ document.positionAt(whereProp.key.start),
341
+ document.positionAt(whereProp.key.end)
342
+ ),
343
+ `'${whereAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
344
+ lsp.DiagnosticSeverity.Error,
345
+ 'sails-lsp'
346
+ )
347
+ )
348
+ }
141
349
  }
142
350
  continue
143
351
  }
144
352
  if (
145
- method !== 'create' &&
146
- method !== 'createEach' &&
147
- queryOptionKeys.includes(attribute)
353
+ (attribute === 'select' || attribute === 'omit') &&
354
+ prop.value &&
355
+ prop.value.type === 'ArrayExpression'
148
356
  ) {
149
- if (
150
- attribute === 'where' &&
151
- prop.value &&
152
- prop.value.type === 'ObjectExpression'
153
- ) {
154
- for (const whereProp of prop.value.properties) {
155
- if (!whereProp.key) continue
156
- const whereAttr =
157
- whereProp.key.name || whereProp.key.value
357
+ for (const el of prop.value.elements) {
358
+ if (!el) continue
359
+ if (el.type === 'Literal' || el.type === 'StringLiteral') {
360
+ const arrAttr = el.value
158
361
  if (
159
362
  !model.attributes ||
160
363
  !Object.prototype.hasOwnProperty.call(
161
364
  model.attributes,
162
- whereAttr
365
+ arrAttr
163
366
  )
164
367
  ) {
165
368
  diagnostics.push(
166
369
  lsp.Diagnostic.create(
167
370
  lsp.Range.create(
168
- document.positionAt(whereProp.key.start),
169
- document.positionAt(whereProp.key.end)
371
+ document.positionAt(el.start),
372
+ document.positionAt(el.end)
170
373
  ),
171
- `'${whereAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
374
+ `'${arrAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
172
375
  lsp.DiagnosticSeverity.Error,
173
376
  'sails-lsp'
174
377
  )
175
378
  )
176
379
  }
177
380
  }
178
- continue
179
381
  }
382
+ continue
383
+ }
384
+ if (attribute === 'sort' && prop.value) {
180
385
  if (
181
- (attribute === 'select' || attribute === 'omit') &&
182
- prop.value &&
183
- prop.value.type === 'ArrayExpression'
386
+ prop.value.type === 'Literal' ||
387
+ prop.value.type === 'StringLiteral'
184
388
  ) {
389
+ const sortStr = prop.value.value
390
+ const sortAttr = sortStr && sortStr.split(' ')[0]
391
+ if (
392
+ sortAttr &&
393
+ (!model.attributes ||
394
+ !Object.prototype.hasOwnProperty.call(
395
+ model.attributes,
396
+ sortAttr
397
+ ))
398
+ ) {
399
+ diagnostics.push(
400
+ lsp.Diagnostic.create(
401
+ lsp.Range.create(
402
+ document.positionAt(prop.value.start),
403
+ document.positionAt(prop.value.end)
404
+ ),
405
+ `'${sortAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
406
+ lsp.DiagnosticSeverity.Error,
407
+ 'sails-lsp'
408
+ )
409
+ )
410
+ }
411
+ continue
412
+ } else if (prop.value.type === 'ArrayExpression') {
185
413
  for (const el of prop.value.elements) {
186
414
  if (!el) continue
187
- if (
415
+ if (el.type === 'ObjectExpression') {
416
+ for (const sortProp of el.properties) {
417
+ if (!sortProp.key) continue
418
+ const sortAttr =
419
+ sortProp.key.name || sortProp.key.value
420
+ if (
421
+ !model.attributes ||
422
+ !Object.prototype.hasOwnProperty.call(
423
+ model.attributes,
424
+ sortAttr
425
+ )
426
+ ) {
427
+ diagnostics.push(
428
+ lsp.Diagnostic.create(
429
+ lsp.Range.create(
430
+ document.positionAt(sortProp.key.start),
431
+ document.positionAt(sortProp.key.end)
432
+ ),
433
+ `'${sortAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
434
+ lsp.DiagnosticSeverity.Error,
435
+ 'sails-lsp'
436
+ )
437
+ )
438
+ }
439
+ }
440
+ } else if (
188
441
  el.type === 'Literal' ||
189
442
  el.type === 'StringLiteral'
190
443
  ) {
191
- const arrAttr = el.value
444
+ const sortStr = el.value
445
+ const sortAttr = sortStr && sortStr.split(' ')[0]
192
446
  if (
193
- !model.attributes ||
194
- !Object.prototype.hasOwnProperty.call(
195
- model.attributes,
196
- arrAttr
197
- )
447
+ sortAttr &&
448
+ (!model.attributes ||
449
+ !Object.prototype.hasOwnProperty.call(
450
+ model.attributes,
451
+ sortAttr
452
+ ))
198
453
  ) {
199
454
  diagnostics.push(
200
455
  lsp.Diagnostic.create(
@@ -202,7 +457,7 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
202
457
  document.positionAt(el.start),
203
458
  document.positionAt(el.end)
204
459
  ),
205
- `'${arrAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
460
+ `'${sortAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
206
461
  lsp.DiagnosticSeverity.Error,
207
462
  'sails-lsp'
208
463
  )
@@ -211,144 +466,56 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
211
466
  }
212
467
  }
213
468
  continue
214
- }
215
- if (attribute === 'sort' && prop.value) {
216
- if (
217
- prop.value.type === 'Literal' ||
218
- prop.value.type === 'StringLiteral'
219
- ) {
220
- const sortStr = prop.value.value
221
- const sortAttr = sortStr && sortStr.split(' ')[0]
469
+ } else if (prop.value.type === 'ObjectExpression') {
470
+ for (const sortProp of prop.value.properties) {
471
+ if (!sortProp.key) continue
472
+ const sortAttr = sortProp.key.name || sortProp.key.value
222
473
  if (
223
- sortAttr &&
224
- (!model.attributes ||
225
- !Object.prototype.hasOwnProperty.call(
226
- model.attributes,
227
- sortAttr
228
- ))
474
+ !model.attributes ||
475
+ !Object.prototype.hasOwnProperty.call(
476
+ model.attributes,
477
+ sortAttr
478
+ )
229
479
  ) {
230
480
  diagnostics.push(
231
481
  lsp.Diagnostic.create(
232
482
  lsp.Range.create(
233
- document.positionAt(prop.value.start),
234
- document.positionAt(prop.value.end)
483
+ document.positionAt(sortProp.key.start),
484
+ document.positionAt(sortProp.key.end)
235
485
  ),
236
- `'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
486
+ `'${sortAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
237
487
  lsp.DiagnosticSeverity.Error,
238
488
  'sails-lsp'
239
489
  )
240
490
  )
241
491
  }
242
- continue
243
- } else if (prop.value.type === 'ArrayExpression') {
244
- for (const el of prop.value.elements) {
245
- if (!el) continue
246
- if (el.type === 'ObjectExpression') {
247
- for (const sortProp of el.properties) {
248
- if (!sortProp.key) continue
249
- const sortAttr =
250
- sortProp.key.name || sortProp.key.value
251
- if (
252
- !model.attributes ||
253
- !Object.prototype.hasOwnProperty.call(
254
- model.attributes,
255
- sortAttr
256
- )
257
- ) {
258
- diagnostics.push(
259
- lsp.Diagnostic.create(
260
- lsp.Range.create(
261
- document.positionAt(sortProp.key.start),
262
- document.positionAt(sortProp.key.end)
263
- ),
264
- `'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
265
- lsp.DiagnosticSeverity.Error,
266
- 'sails-lsp'
267
- )
268
- )
269
- }
270
- }
271
- } else if (
272
- el.type === 'Literal' ||
273
- el.type === 'StringLiteral'
274
- ) {
275
- const sortStr = el.value
276
- const sortAttr = sortStr && sortStr.split(' ')[0]
277
- if (
278
- sortAttr &&
279
- (!model.attributes ||
280
- !Object.prototype.hasOwnProperty.call(
281
- model.attributes,
282
- sortAttr
283
- ))
284
- ) {
285
- diagnostics.push(
286
- lsp.Diagnostic.create(
287
- lsp.Range.create(
288
- document.positionAt(el.start),
289
- document.positionAt(el.end)
290
- ),
291
- `'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
292
- lsp.DiagnosticSeverity.Error,
293
- 'sails-lsp'
294
- )
295
- )
296
- }
297
- }
298
- }
299
- continue
300
- } else if (prop.value.type === 'ObjectExpression') {
301
- for (const sortProp of prop.value.properties) {
302
- if (!sortProp.key) continue
303
- const sortAttr = sortProp.key.name || sortProp.key.value
304
- if (
305
- !model.attributes ||
306
- !Object.prototype.hasOwnProperty.call(
307
- model.attributes,
308
- sortAttr
309
- )
310
- ) {
311
- diagnostics.push(
312
- lsp.Diagnostic.create(
313
- lsp.Range.create(
314
- document.positionAt(sortProp.key.start),
315
- document.positionAt(sortProp.key.end)
316
- ),
317
- `'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
318
- lsp.DiagnosticSeverity.Error,
319
- 'sails-lsp'
320
- )
321
- )
322
- }
323
- }
324
- continue
325
492
  }
493
+ continue
326
494
  }
327
- // For all other query option keys, skip validation
328
- continue
329
495
  }
330
- // --- END ROBUST FIX ---
331
- // Only validate top-level for create/createEach
332
- if (
333
- (method === 'create' || method === 'createEach') &&
334
- (!model.attributes ||
335
- !Object.prototype.hasOwnProperty.call(
336
- model.attributes,
337
- attribute
338
- ))
339
- ) {
340
- diagnostics.push(
341
- lsp.Diagnostic.create(
342
- lsp.Range.create(
343
- document.positionAt(prop.key.start),
344
- document.positionAt(prop.key.end)
345
- ),
346
- `'${attribute}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
347
- lsp.DiagnosticSeverity.Error,
348
- 'sails-lsp'
349
- )
496
+ // For all other query option keys, skip validation
497
+ continue
498
+ }
499
+ // Only validate top-level for create/createEach
500
+ if (
501
+ (method === 'create' || method === 'createEach') &&
502
+ (!model.attributes ||
503
+ !Object.prototype.hasOwnProperty.call(
504
+ model.attributes,
505
+ attribute
506
+ ))
507
+ ) {
508
+ diagnostics.push(
509
+ lsp.Diagnostic.create(
510
+ lsp.Range.create(
511
+ document.positionAt(prop.key.start),
512
+ document.positionAt(prop.key.end)
513
+ ),
514
+ `'${attribute}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
515
+ lsp.DiagnosticSeverity.Error,
516
+ 'sails-lsp'
350
517
  )
351
- }
518
+ )
352
519
  }
353
520
  }
354
521
  }