@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.
- package/SailsParser.js +57 -12
- package/completions/model-attribute-props-completion.js +20 -19
- package/completions/model-attributes-completion.js +350 -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-page.js +40 -26
- package/go-to-definitions/go-to-view.js +42 -24
- package/index.js +1 -1
- 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 +162 -55
- package/validators/validate-model-exist.js +14 -0
- package/validators/validate-required-helper-input.js +99 -39
|
@@ -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,
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
}
|