@sailshq/language-server 0.3.1 → 0.4.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.
@@ -2,6 +2,118 @@ const lsp = require('vscode-languageserver/node')
2
2
  const acorn = require('acorn')
3
3
  const walk = require('acorn-walk')
4
4
 
5
+ /**
6
+ * Waterline query modifiers and operators
7
+ */
8
+ const WATERLINE_MODIFIERS = ['or', 'and', 'not']
9
+ const WATERLINE_OPERATORS = [
10
+ '<',
11
+ '<=',
12
+ '>',
13
+ '>=',
14
+ '!=',
15
+ 'nin',
16
+ 'in',
17
+ 'contains',
18
+ 'startsWith',
19
+ 'endsWith',
20
+ 'like',
21
+ '!'
22
+ ]
23
+
24
+ /**
25
+ * Helper function to recursively validate criteria attributes
26
+ * @param {Object} objNode - AST ObjectExpression node
27
+ * @param {Object} model - Model with attributes
28
+ * @param {TextDocument} document - Text document
29
+ * @param {Array} diagnostics - Diagnostics array to push to
30
+ * @param {string} effectiveModelName - Model name for error messages
31
+ */
32
+ function validateCriteriaAttributes(
33
+ objNode,
34
+ model,
35
+ document,
36
+ diagnostics,
37
+ effectiveModelName
38
+ ) {
39
+ if (!objNode || objNode.type !== 'ObjectExpression' || !objNode.properties) {
40
+ return
41
+ }
42
+
43
+ for (const prop of objNode.properties) {
44
+ if (!prop.key) continue
45
+ const attrName = prop.key.name || prop.key.value
46
+
47
+ if (WATERLINE_MODIFIERS.includes(attrName)) {
48
+ if (
49
+ prop.value &&
50
+ prop.value.type === 'ArrayExpression' &&
51
+ prop.value.elements
52
+ ) {
53
+ for (const el of prop.value.elements) {
54
+ if (el && el.type === 'ObjectExpression') {
55
+ validateCriteriaAttributes(
56
+ el,
57
+ model,
58
+ document,
59
+ diagnostics,
60
+ effectiveModelName
61
+ )
62
+ }
63
+ }
64
+ }
65
+ continue
66
+ }
67
+
68
+ if (
69
+ prop.value &&
70
+ prop.value.type === 'ObjectExpression' &&
71
+ prop.value.properties &&
72
+ prop.value.properties.length > 0
73
+ ) {
74
+ const firstKey =
75
+ prop.value.properties[0].key?.name ||
76
+ prop.value.properties[0].key?.value
77
+ if (WATERLINE_OPERATORS.includes(firstKey)) {
78
+ if (
79
+ !model.attributes ||
80
+ !Object.prototype.hasOwnProperty.call(model.attributes, attrName)
81
+ ) {
82
+ diagnostics.push(
83
+ lsp.Diagnostic.create(
84
+ lsp.Range.create(
85
+ document.positionAt(prop.key.start),
86
+ document.positionAt(prop.key.end)
87
+ ),
88
+ `'${attrName}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
89
+ lsp.DiagnosticSeverity.Error,
90
+ 'sails-lsp'
91
+ )
92
+ )
93
+ }
94
+ continue
95
+ }
96
+ }
97
+
98
+ if (
99
+ !model.attributes ||
100
+ !Object.prototype.hasOwnProperty.call(model.attributes, attrName)
101
+ ) {
102
+ diagnostics.push(
103
+ lsp.Diagnostic.create(
104
+ lsp.Range.create(
105
+ document.positionAt(prop.key.start),
106
+ document.positionAt(prop.key.end)
107
+ ),
108
+ `'${attrName}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
109
+ lsp.DiagnosticSeverity.Error,
110
+ 'sails-lsp'
111
+ )
112
+ )
113
+ }
114
+ }
115
+ }
116
+
5
117
  /**
6
118
  * Validate if a Waterline model attribute exists when used in criteria or chainable methods.
7
119
  * @param {TextDocument} document - The text document to validate.
@@ -26,6 +138,36 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
26
138
  return typeMap.models[upper]
27
139
  }
28
140
 
141
+ // Helper to check if an identifier is likely a Sails model
142
+ function isLikelyModel(name) {
143
+ if (!name) return false
144
+
145
+ // Exclude common globals and libraries
146
+ const knownGlobals = [
147
+ '_',
148
+ 'sails',
149
+ 'require',
150
+ 'module',
151
+ 'exports',
152
+ 'console',
153
+ 'process'
154
+ ]
155
+ if (knownGlobals.includes(name)) {
156
+ return false
157
+ }
158
+
159
+ // Check if it's in the typeMap models (case-insensitive)
160
+ const upper = name.charAt(0).toUpperCase() + name.slice(1)
161
+ if (typeMap.models && typeMap.models[upper]) {
162
+ return true
163
+ }
164
+ // Also check lowercase version
165
+ if (typeMap.models && typeMap.models[name.toLowerCase()]) {
166
+ return true
167
+ }
168
+ return false
169
+ }
170
+
29
171
  // AST-based: Validate Model.create({ ... }) and similar
30
172
  try {
31
173
  const ast = acorn.parse(text, {
@@ -53,6 +195,9 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
53
195
  break
54
196
  }
55
197
  }
198
+ // Only proceed if this is actually a known Sails model
199
+ if (!isLikelyModel(effectiveModelName)) return
200
+
56
201
  const model = getModelByName(effectiveModelName)
57
202
  if (!model) return
58
203
 
@@ -287,66 +432,28 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
287
432
  'and',
288
433
  'not'
289
434
  ]
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
- ) {
296
- if (
297
- !model.attributes ||
298
- !Object.prototype.hasOwnProperty.call(
299
- model.attributes,
300
- attribute
435
+ // For non-create methods, use the helper to validate criteria
436
+ if (method !== 'create' && method !== 'createEach') {
437
+ if (!queryOptionKeys.includes(attribute)) {
438
+ validateCriteriaAttributes(
439
+ { type: 'ObjectExpression', properties: [prop] },
440
+ model,
441
+ document,
442
+ diagnostics,
443
+ effectiveModelName
301
444
  )
302
- ) {
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'
312
- )
313
- )
314
- }
315
- continue
316
- }
317
- if (
318
- method !== 'create' &&
319
- method !== 'createEach' &&
320
- queryOptionKeys.includes(attribute)
321
- ) {
322
- if (
445
+ } else if (
323
446
  attribute === 'where' &&
324
447
  prop.value &&
325
448
  prop.value.type === 'ObjectExpression'
326
449
  ) {
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
335
- )
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
- }
349
- }
450
+ validateCriteriaAttributes(
451
+ prop.value,
452
+ model,
453
+ document,
454
+ diagnostics,
455
+ effectiveModelName
456
+ )
350
457
  continue
351
458
  }
352
459
  if (
@@ -19,12 +19,26 @@ module.exports = function validateModelExist(document, typeMap) {
19
19
  if (!name) return false
20
20
  return !!modelMap[name.toLowerCase()]
21
21
  }
22
+
23
+ const knownGlobals = [
24
+ '_',
25
+ 'sails',
26
+ 'require',
27
+ 'module',
28
+ 'exports',
29
+ 'console',
30
+ 'process'
31
+ ]
32
+
22
33
  // User.find() or User.create() etc
23
34
  const modelCallRegex =
24
35
  /\b([A-Za-z0-9_]+)\s*\.(?:find|findOne|create|createEach|update|destroy|count|sum|where|findOrCreate)\s*\(/g
25
36
  let match
26
37
  while ((match = modelCallRegex.exec(text)) !== null) {
27
38
  const modelName = match[1]
39
+ if (knownGlobals.includes(modelName)) {
40
+ continue
41
+ }
28
42
  if (!modelExists(modelName)) {
29
43
  diagnostics.push(
30
44
  lsp.Diagnostic.create(
@@ -1,49 +1,109 @@
1
1
  const lsp = require('vscode-languageserver/node')
2
+ const acorn = require('acorn')
3
+ const walk = require('acorn-walk')
2
4
 
3
5
  module.exports = function validateRequiredHelperInput(document, typeMap) {
4
6
  const diagnostics = []
5
7
  const text = document.getText()
6
8
 
7
- // Regex to match sails.helpers.foo.bar.with({ ... })
8
- // Captures: 1) helper path, 2) object literal content
9
- const regex = /sails\.helpers((?:\.[a-zA-Z0-9_]+)+)\.with\s*\(\s*\{([^}]*)\}/g
10
- let match
11
- while ((match = regex.exec(text)) !== null) {
12
- // Build helper name: e.g. .foo.bar => foo/bar
13
- const segments = match[1].split('.').filter(Boolean)
14
- const toKebab = (s) => s.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
15
- const fullHelperName = segments.map(toKebab).join('/')
16
- const helperInfo = typeMap.helpers && typeMap.helpers[fullHelperName]
17
- if (!helperInfo || !helperInfo.inputs) continue
18
-
19
- // Find all property names in the object literal
20
- const propsRegex = /([a-zA-Z0-9_]+)\s*:/g
21
- let propMatch
22
- const providedKeys = new Set()
23
- while ((propMatch = propsRegex.exec(match[2])) !== null) {
24
- providedKeys.add(propMatch[1])
25
- }
26
- // Check for missing required inputs (support boolean or string 'required')
27
- for (const [inputKey, inputDef] of Object.entries(helperInfo.inputs)) {
28
- const isRequired =
29
- inputDef && (inputDef.required === true || inputDef.required === 'true')
30
- if (isRequired && !providedKeys.has(inputKey)) {
31
- // Find the start/end of the object literal for the diagnostic range
32
- const objStart = match.index + match[0].indexOf('{')
33
- const objEnd = objStart + match[2].length + 1 // +1 for closing }
34
- diagnostics.push(
35
- lsp.Diagnostic.create(
36
- lsp.Range.create(
37
- document.positionAt(objStart),
38
- document.positionAt(objEnd)
39
- ),
40
- `Missing required input '${inputKey}' for helper '${fullHelperName}'.`,
41
- lsp.DiagnosticSeverity.Error,
42
- 'sails-lsp'
43
- )
44
- )
9
+ try {
10
+ const ast = acorn.parse(text, {
11
+ ecmaVersion: 'latest',
12
+ sourceType: 'module'
13
+ })
14
+
15
+ walk.simple(ast, {
16
+ CallExpression(node) {
17
+ // Match sails.helpers.foo.bar.with({ ... })
18
+ if (
19
+ node.callee &&
20
+ node.callee.type === 'MemberExpression' &&
21
+ node.callee.property.name === 'with' &&
22
+ node.callee.object &&
23
+ node.callee.object.type === 'MemberExpression'
24
+ ) {
25
+ // Extract helper path from sails.helpers.foo.bar
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
+ // Get the object argument to .with()
33
+ const objArg = node.arguments[0]
34
+ if (!objArg || objArg.type !== 'ObjectExpression') return
35
+
36
+ // Collect provided keys (handles both regular and shorthand properties)
37
+ const providedKeys = new Set()
38
+ for (const prop of objArg.properties) {
39
+ if (prop.type === 'Property') {
40
+ if (prop.key.type === 'Identifier') {
41
+ providedKeys.add(prop.key.name)
42
+ } else if (prop.key.type === 'Literal') {
43
+ providedKeys.add(prop.key.value)
44
+ }
45
+ }
46
+ }
47
+
48
+ // Check for missing required inputs
49
+ for (const [inputKey, inputDef] of Object.entries(
50
+ helperInfo.inputs
51
+ )) {
52
+ const isRequired =
53
+ inputDef &&
54
+ (inputDef.required === true || inputDef.required === 'true')
55
+ if (isRequired && !providedKeys.has(inputKey)) {
56
+ diagnostics.push(
57
+ lsp.Diagnostic.create(
58
+ lsp.Range.create(
59
+ document.positionAt(objArg.start),
60
+ document.positionAt(objArg.end)
61
+ ),
62
+ `Missing required input '${inputKey}' for helper '${helperPath}'.`,
63
+ lsp.DiagnosticSeverity.Error,
64
+ 'sails-lsp'
65
+ )
66
+ )
67
+ }
68
+ }
69
+ }
45
70
  }
46
- }
71
+ })
72
+ } catch (error) {
73
+ // Ignore parse errors
47
74
  }
75
+
48
76
  return diagnostics
49
77
  }
78
+
79
+ function extractHelperPath(node) {
80
+ // Walk up the member expression to extract the full helper path
81
+ const segments = []
82
+ let current = node
83
+
84
+ // Collect all segments until we reach sails.helpers
85
+ while (current && current.type === 'MemberExpression') {
86
+ if (current.property && current.property.type === 'Identifier') {
87
+ const propName = current.property.name
88
+ // Stop when we reach 'helpers'
89
+ if (propName === 'helpers') {
90
+ // Check if the object is 'sails'
91
+ if (
92
+ current.object &&
93
+ current.object.type === 'Identifier' &&
94
+ current.object.name === 'sails'
95
+ ) {
96
+ // Valid sails.helpers path found
97
+ const toKebab = (s) =>
98
+ s.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
99
+ return segments.map(toKebab).join('/')
100
+ }
101
+ return null
102
+ }
103
+ segments.unshift(propName)
104
+ }
105
+ current = current.object
106
+ }
107
+
108
+ return null
109
+ }