@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.
@@ -1,34 +1,97 @@
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 validateDataType(document, typeMap) {
4
6
  const diagnostics = []
5
-
6
7
  const text = document.getText()
7
8
 
8
- // Regex to match lines like: type: 'string' or type: "number"
9
- const regex = /type\s*:\s*['"]([a-zA-Z0-9_-]+)['"]/g
10
-
11
- let match
12
- while ((match = regex.exec(text)) !== null) {
13
- const dataType = match[1]
14
- const typeStart = match.index + match[0].indexOf(dataType)
15
- const typeEnd = typeStart + dataType.length
16
-
17
- const isValid = typeMap.dataTypes.some((dt) => dt.type === dataType)
18
-
19
- if (!isValid) {
20
- diagnostics.push(
21
- lsp.Diagnostic.create(
22
- lsp.Range.create(
23
- document.positionAt(typeStart),
24
- document.positionAt(typeEnd)
25
- ),
26
- `'${dataType}' is not a recognized data type. Valid data types are: ${typeMap.dataTypes.map((dataType) => dataType.type).join(', ')}.`,
27
- lsp.DiagnosticSeverity.Error,
28
- 'sails-lsp'
29
- )
30
- )
31
- }
9
+ try {
10
+ const ast = acorn.parse(text, {
11
+ ecmaVersion: 'latest',
12
+ sourceType: 'module'
13
+ })
14
+
15
+ walk.simple(ast, {
16
+ ObjectExpression(node) {
17
+ // Check if this object has both 'type' and other properties that suggest it's a definition
18
+ // (like 'required', 'description', 'allowNull', 'defaultsTo', 'example', etc.)
19
+ let hasTypeProperty = false
20
+ let typePropertyNode = null
21
+ let typeValue = null
22
+ let hasDefinitionProperties = false
23
+
24
+ for (const prop of node.properties) {
25
+ if (prop.type !== 'Property') continue
26
+
27
+ const keyName = prop.key.name || prop.key.value
28
+
29
+ // Check if this is a 'type' property
30
+ if (keyName === 'type' && prop.value.type === 'Literal') {
31
+ hasTypeProperty = true
32
+ typePropertyNode = prop.value
33
+ typeValue = prop.value.value
34
+ }
35
+
36
+ // Check for properties that indicate this is a model/action/helper definition
37
+ if (
38
+ [
39
+ 'required',
40
+ 'description',
41
+ 'allowNull',
42
+ 'defaultsTo',
43
+ 'columnName',
44
+ 'columnType',
45
+ 'autoMigrations',
46
+ 'autoCreatedAt',
47
+ 'autoUpdatedAt',
48
+ 'model',
49
+ 'collection',
50
+ 'via',
51
+ 'through',
52
+ 'unique',
53
+ 'isEmail',
54
+ 'isURL',
55
+ 'isIn',
56
+ 'min',
57
+ 'max',
58
+ 'minLength',
59
+ 'maxLength',
60
+ 'example',
61
+ 'validations',
62
+ 'regex',
63
+ 'extendedDescription',
64
+ 'moreInfoUrl',
65
+ 'whereToGet'
66
+ ].includes(keyName)
67
+ ) {
68
+ hasDefinitionProperties = true
69
+ }
70
+ }
71
+
72
+ // Only validate if this looks like an attribute/input definition
73
+ if (hasTypeProperty && hasDefinitionProperties && typeValue) {
74
+ const isValid = typeMap.dataTypes.some((dt) => dt.type === typeValue)
75
+
76
+ if (!isValid) {
77
+ diagnostics.push(
78
+ lsp.Diagnostic.create(
79
+ lsp.Range.create(
80
+ document.positionAt(typePropertyNode.start),
81
+ document.positionAt(typePropertyNode.end)
82
+ ),
83
+ `'${typeValue}' is not a recognized data type. Valid data types are: ${typeMap.dataTypes.map((dataType) => dataType.type).join(', ')}.`,
84
+ lsp.DiagnosticSeverity.Error,
85
+ 'sails-lsp'
86
+ )
87
+ )
88
+ }
89
+ }
90
+ }
91
+ })
92
+ } catch (error) {
93
+ // Ignore parse errors
32
94
  }
95
+
33
96
  return diagnostics
34
97
  }
@@ -1,42 +1,108 @@
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 validateHelperInputExist(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
- while ((propMatch = propsRegex.exec(match[2])) !== null) {
23
- const key = propMatch[1]
24
- if (!Object.prototype.hasOwnProperty.call(helperInfo.inputs, key)) {
25
- const start = match.index + match[0].indexOf(key)
26
- const end = start + key.length
27
- diagnostics.push(
28
- lsp.Diagnostic.create(
29
- lsp.Range.create(
30
- document.positionAt(start),
31
- document.positionAt(end)
32
- ),
33
- `Unknown input property '${key}' for helper '${fullHelperName}'.`,
34
- lsp.DiagnosticSeverity.Error,
35
- 'sails-lsp'
36
- )
37
- )
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
+ // Validate each property in the object
37
+ for (const prop of objArg.properties) {
38
+ // Handle both regular properties and shorthand properties
39
+ let key
40
+ if (prop.type === 'Property') {
41
+ if (prop.key.type === 'Identifier') {
42
+ key = prop.key.name
43
+ } else if (prop.key.type === 'Literal') {
44
+ key = prop.key.value
45
+ }
46
+ } else if (prop.type === 'SpreadElement') {
47
+ // Skip spread elements
48
+ continue
49
+ }
50
+
51
+ if (!key) continue
52
+
53
+ // Check if this key exists in the helper's inputs
54
+ if (!Object.prototype.hasOwnProperty.call(helperInfo.inputs, key)) {
55
+ diagnostics.push(
56
+ lsp.Diagnostic.create(
57
+ lsp.Range.create(
58
+ document.positionAt(prop.key.start),
59
+ document.positionAt(prop.key.end)
60
+ ),
61
+ `Unknown input property '${key}' for helper '${helperPath}'.`,
62
+ lsp.DiagnosticSeverity.Error,
63
+ 'sails-lsp'
64
+ )
65
+ )
66
+ }
67
+ }
68
+ }
38
69
  }
39
- }
70
+ })
71
+ } catch (error) {
72
+ // Ignore parse errors
40
73
  }
74
+
41
75
  return diagnostics
42
76
  }
77
+
78
+ function extractHelperPath(node) {
79
+ // Walk up the member expression to extract the full helper path
80
+ const segments = []
81
+ let current = node
82
+
83
+ // Collect all segments until we reach sails.helpers
84
+ while (current && current.type === 'MemberExpression') {
85
+ if (current.property && current.property.type === 'Identifier') {
86
+ const propName = current.property.name
87
+ // Stop when we reach 'helpers'
88
+ if (propName === 'helpers') {
89
+ // Check if the object is 'sails'
90
+ if (
91
+ current.object &&
92
+ current.object.type === 'Identifier' &&
93
+ current.object.name === 'sails'
94
+ ) {
95
+ // Valid sails.helpers path found
96
+ const toKebab = (s) =>
97
+ s.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
98
+ return segments.map(toKebab).join('/')
99
+ }
100
+ return null
101
+ }
102
+ segments.unshift(propName)
103
+ }
104
+ current = current.object
105
+ }
106
+
107
+ return null
108
+ }
@@ -2,6 +2,126 @@ 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
+ } else if (prop.value && prop.value.type === 'ObjectExpression') {
65
+ validateCriteriaAttributes(
66
+ prop.value,
67
+ model,
68
+ document,
69
+ diagnostics,
70
+ effectiveModelName
71
+ )
72
+ }
73
+ continue
74
+ }
75
+
76
+ if (
77
+ prop.value &&
78
+ prop.value.type === 'ObjectExpression' &&
79
+ prop.value.properties &&
80
+ prop.value.properties.length > 0
81
+ ) {
82
+ const firstKey =
83
+ prop.value.properties[0].key?.name ||
84
+ prop.value.properties[0].key?.value
85
+ if (WATERLINE_OPERATORS.includes(firstKey)) {
86
+ if (
87
+ !model.attributes ||
88
+ !Object.prototype.hasOwnProperty.call(model.attributes, attrName)
89
+ ) {
90
+ diagnostics.push(
91
+ lsp.Diagnostic.create(
92
+ lsp.Range.create(
93
+ document.positionAt(prop.key.start),
94
+ document.positionAt(prop.key.end)
95
+ ),
96
+ `'${attrName}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
97
+ lsp.DiagnosticSeverity.Error,
98
+ 'sails-lsp'
99
+ )
100
+ )
101
+ }
102
+ continue
103
+ }
104
+ }
105
+
106
+ if (
107
+ !model.attributes ||
108
+ !Object.prototype.hasOwnProperty.call(model.attributes, attrName)
109
+ ) {
110
+ diagnostics.push(
111
+ lsp.Diagnostic.create(
112
+ lsp.Range.create(
113
+ document.positionAt(prop.key.start),
114
+ document.positionAt(prop.key.end)
115
+ ),
116
+ `'${attrName}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
117
+ lsp.DiagnosticSeverity.Error,
118
+ 'sails-lsp'
119
+ )
120
+ )
121
+ }
122
+ }
123
+ }
124
+
5
125
  /**
6
126
  * Validate if a Waterline model attribute exists when used in criteria or chainable methods.
7
127
  * @param {TextDocument} document - The text document to validate.
@@ -26,6 +146,36 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
26
146
  return typeMap.models[upper]
27
147
  }
28
148
 
149
+ // Helper to check if an identifier is likely a Sails model
150
+ function isLikelyModel(name) {
151
+ if (!name) return false
152
+
153
+ // Exclude common globals and libraries
154
+ const knownGlobals = [
155
+ '_',
156
+ 'sails',
157
+ 'require',
158
+ 'module',
159
+ 'exports',
160
+ 'console',
161
+ 'process'
162
+ ]
163
+ if (knownGlobals.includes(name)) {
164
+ return false
165
+ }
166
+
167
+ // Check if it's in the typeMap models (case-insensitive)
168
+ const upper = name.charAt(0).toUpperCase() + name.slice(1)
169
+ if (typeMap.models && typeMap.models[upper]) {
170
+ return true
171
+ }
172
+ // Also check lowercase version
173
+ if (typeMap.models && typeMap.models[name.toLowerCase()]) {
174
+ return true
175
+ }
176
+ return false
177
+ }
178
+
29
179
  // AST-based: Validate Model.create({ ... }) and similar
30
180
  try {
31
181
  const ast = acorn.parse(text, {
@@ -53,6 +203,9 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
53
203
  break
54
204
  }
55
205
  }
206
+ // Only proceed if this is actually a known Sails model
207
+ if (!isLikelyModel(effectiveModelName)) return
208
+
56
209
  const model = getModelByName(effectiveModelName)
57
210
  if (!model) return
58
211
 
@@ -287,63 +440,42 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
287
440
  'and',
288
441
  'not'
289
442
  ]
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
301
- )
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
- )
443
+ // For non-create methods, use the helper to validate criteria
444
+ if (method !== 'create' && method !== 'createEach') {
445
+ if (!queryOptionKeys.includes(attribute)) {
446
+ validateCriteriaAttributes(
447
+ { type: 'ObjectExpression', properties: [prop] },
448
+ model,
449
+ document,
450
+ diagnostics,
451
+ effectiveModelName
313
452
  )
314
- }
315
- continue
316
- }
317
- if (
318
- method !== 'create' &&
319
- method !== 'createEach' &&
320
- queryOptionKeys.includes(attribute)
321
- ) {
322
- if (
453
+ } else if (
323
454
  attribute === 'where' &&
324
455
  prop.value &&
325
456
  prop.value.type === 'ObjectExpression'
326
457
  ) {
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
- )
458
+ validateCriteriaAttributes(
459
+ prop.value,
460
+ model,
461
+ document,
462
+ diagnostics,
463
+ effectiveModelName
464
+ )
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
347
479
  )
348
480
  }
349
481
  }
@@ -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(