@sailshq/language-server 0.1.0 → 0.2.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 +325 -76
- package/completions/data-types-completion.js +6 -5
- package/completions/helper-inputs-completion.js +91 -0
- package/completions/helpers-completion.js +85 -0
- package/completions/input-props-completion.js +19 -23
- package/completions/model-attribute-props-completion.js +36 -32
- package/completions/model-attributes-completion.js +103 -10
- package/completions/model-methods-completion.js +20 -10
- package/go-to-definitions/go-to-helper.js +34 -29
- package/index.js +12 -3
- package/package.json +1 -1
- package/validators/validate-document.js +23 -1
- package/validators/validate-helper-input-exist.js +42 -0
- package/validators/validate-model-attribute-exist.js +278 -109
- package/validators/validate-model-exist.js +64 -0
- package/validators/validate-required-helper-input.js +49 -0
- package/validators/validate-required-model-attribute.js +56 -0
- package/validators/validate-view-exist.js +86 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
const acorn = require('acorn')
|
|
3
|
+
const walk = require('acorn-walk')
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Validate if a Waterline model attribute exists when used in criteria or chainable methods.
|
|
@@ -10,119 +12,286 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
|
|
|
10
12
|
const diagnostics = []
|
|
11
13
|
const text = document.getText()
|
|
12
14
|
|
|
13
|
-
//
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const arrayChainRegex = /\.(select|omit)\s*\(\s*\[([^\]]*)\]/g
|
|
19
|
-
|
|
20
|
-
// Chainable: .populate('attr')
|
|
21
|
-
const populateRegex = /\.populate\s*\(\s*['"]([A-Za-z0-9_]+)['"]\s*\)/g
|
|
22
|
-
|
|
23
|
-
let match
|
|
24
|
-
|
|
25
|
-
// Criteria methods
|
|
26
|
-
while ((match = criteriaRegex.exec(text)) !== null) {
|
|
27
|
-
const modelName = match[1]
|
|
28
|
-
const attribute = match[3]
|
|
29
|
-
const attrStart = match.index + match[0].lastIndexOf(attribute)
|
|
30
|
-
const attrEnd = attrStart + attribute.length
|
|
31
|
-
|
|
32
|
-
const model = typeMap.models && typeMap.models[modelName]
|
|
33
|
-
if (!model) continue
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
!model.attributes ||
|
|
37
|
-
!Object.prototype.hasOwnProperty.call(model.attributes, attribute)
|
|
38
|
-
) {
|
|
39
|
-
diagnostics.push(
|
|
40
|
-
lsp.Diagnostic.create(
|
|
41
|
-
lsp.Range.create(
|
|
42
|
-
document.positionAt(attrStart),
|
|
43
|
-
document.positionAt(attrEnd)
|
|
44
|
-
),
|
|
45
|
-
`'${attribute}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
46
|
-
lsp.DiagnosticSeverity.Error,
|
|
47
|
-
'sails-lsp'
|
|
48
|
-
)
|
|
49
|
-
)
|
|
15
|
+
// Build a lowercased model map for robust case-insensitive lookup
|
|
16
|
+
const modelMap = {}
|
|
17
|
+
if (typeMap.models) {
|
|
18
|
+
for (const key of Object.keys(typeMap.models)) {
|
|
19
|
+
modelMap[key.toLowerCase()] = typeMap.models[key]
|
|
50
20
|
}
|
|
51
21
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
// Try to find the model name by searching backwards for ModelName.
|
|
58
|
-
// This is a heuristic and may not be perfect.
|
|
59
|
-
const before = text.slice(0, match.index)
|
|
60
|
-
const modelMatch = /([A-Za-z0-9_]+)\s*\.\s*$/.exec(
|
|
61
|
-
before.split('\n').pop() || ''
|
|
62
|
-
)
|
|
63
|
-
const modelName = modelMatch && modelMatch[1]
|
|
64
|
-
if (!modelName) continue
|
|
65
|
-
const model = typeMap.models && typeMap.models[modelName]
|
|
66
|
-
if (!model) continue
|
|
67
|
-
|
|
68
|
-
// Extract attribute names from the array string
|
|
69
|
-
const attrRegex = /['"]([A-Za-z0-9_]+)['"]/g
|
|
70
|
-
let attrMatch
|
|
71
|
-
while ((attrMatch = attrRegex.exec(attrsString)) !== null) {
|
|
72
|
-
const attribute = attrMatch[1]
|
|
73
|
-
const attrStart = match.index + match[0].indexOf(attribute)
|
|
74
|
-
const attrEnd = attrStart + attribute.length
|
|
75
|
-
if (
|
|
76
|
-
!model.attributes ||
|
|
77
|
-
!Object.prototype.hasOwnProperty.call(model.attributes, attribute)
|
|
78
|
-
) {
|
|
79
|
-
diagnostics.push(
|
|
80
|
-
lsp.Diagnostic.create(
|
|
81
|
-
lsp.Range.create(
|
|
82
|
-
document.positionAt(attrStart),
|
|
83
|
-
document.positionAt(attrEnd)
|
|
84
|
-
),
|
|
85
|
-
`'${attribute}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
86
|
-
lsp.DiagnosticSeverity.Error,
|
|
87
|
-
'sails-lsp'
|
|
88
|
-
)
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
22
|
+
// Helper function to get model by name, case-insensitive
|
|
23
|
+
function getModelByName(name) {
|
|
24
|
+
if (!name) return undefined
|
|
25
|
+
const upper = name.charAt(0).toUpperCase() + name.slice(1)
|
|
26
|
+
return typeMap.models[upper]
|
|
92
27
|
}
|
|
93
28
|
|
|
94
|
-
// .
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
29
|
+
// AST-based: Validate Model.create({ ... }) and similar
|
|
30
|
+
try {
|
|
31
|
+
const ast = acorn.parse(text, {
|
|
32
|
+
ecmaVersion: 'latest',
|
|
33
|
+
sourceType: 'module'
|
|
34
|
+
})
|
|
35
|
+
walk.simple(ast, {
|
|
36
|
+
CallExpression(node) {
|
|
37
|
+
if (
|
|
38
|
+
node.callee &&
|
|
39
|
+
node.callee.type === 'MemberExpression' &&
|
|
40
|
+
node.arguments &&
|
|
41
|
+
node.arguments.length > 0 &&
|
|
42
|
+
node.arguments[0].type === 'ObjectExpression'
|
|
43
|
+
) {
|
|
44
|
+
const method = node.callee.property.name
|
|
45
|
+
const modelName = node.callee.object.name
|
|
46
|
+
// Only check for Waterline methods
|
|
47
|
+
if (
|
|
48
|
+
[
|
|
49
|
+
'create',
|
|
50
|
+
'createEach',
|
|
51
|
+
'count',
|
|
52
|
+
'find',
|
|
53
|
+
'findOne',
|
|
54
|
+
'update',
|
|
55
|
+
'destroy',
|
|
56
|
+
'where',
|
|
57
|
+
'findOrCreate',
|
|
58
|
+
'sum'
|
|
59
|
+
].includes(method)
|
|
60
|
+
) {
|
|
61
|
+
const model = getModelByName(modelName)
|
|
62
|
+
if (!model) return
|
|
63
|
+
for (const prop of node.arguments[0].properties) {
|
|
64
|
+
const attribute = prop.key && (prop.key.name || prop.key.value)
|
|
65
|
+
const queryOptionKeys = [
|
|
66
|
+
'where',
|
|
67
|
+
'select',
|
|
68
|
+
'omit',
|
|
69
|
+
'sort',
|
|
70
|
+
'limit',
|
|
71
|
+
'skip',
|
|
72
|
+
'page',
|
|
73
|
+
'populate',
|
|
74
|
+
'groupBy',
|
|
75
|
+
'having',
|
|
76
|
+
'sum',
|
|
77
|
+
'average',
|
|
78
|
+
'min',
|
|
79
|
+
'max',
|
|
80
|
+
'distinct',
|
|
81
|
+
'meta'
|
|
82
|
+
]
|
|
83
|
+
// --- FINAL FIX: treat 'where' exactly like 'sort', 'select', 'omit' for non-create methods ---
|
|
84
|
+
if (
|
|
85
|
+
method !== 'create' &&
|
|
86
|
+
method !== 'createEach' &&
|
|
87
|
+
queryOptionKeys.includes(attribute)
|
|
88
|
+
) {
|
|
89
|
+
if (
|
|
90
|
+
attribute === 'where' &&
|
|
91
|
+
prop.value &&
|
|
92
|
+
prop.value.type === 'ObjectExpression'
|
|
93
|
+
) {
|
|
94
|
+
for (const whereProp of prop.value.properties) {
|
|
95
|
+
if (!whereProp.key) continue
|
|
96
|
+
const whereAttr = whereProp.key.name || whereProp.key.value
|
|
97
|
+
if (
|
|
98
|
+
!model.attributes ||
|
|
99
|
+
!Object.prototype.hasOwnProperty.call(
|
|
100
|
+
model.attributes,
|
|
101
|
+
whereAttr
|
|
102
|
+
)
|
|
103
|
+
) {
|
|
104
|
+
diagnostics.push(
|
|
105
|
+
lsp.Diagnostic.create(
|
|
106
|
+
lsp.Range.create(
|
|
107
|
+
document.positionAt(whereProp.key.start),
|
|
108
|
+
document.positionAt(whereProp.key.end)
|
|
109
|
+
),
|
|
110
|
+
`'${whereAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
111
|
+
lsp.DiagnosticSeverity.Error,
|
|
112
|
+
'sails-lsp'
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
(attribute === 'select' || attribute === 'omit') &&
|
|
121
|
+
prop.value &&
|
|
122
|
+
prop.value.type === 'ArrayExpression'
|
|
123
|
+
) {
|
|
124
|
+
for (const el of prop.value.elements) {
|
|
125
|
+
if (!el) continue
|
|
126
|
+
if (el.type === 'Literal' || el.type === 'StringLiteral') {
|
|
127
|
+
const arrAttr = el.value
|
|
128
|
+
if (
|
|
129
|
+
!model.attributes ||
|
|
130
|
+
!Object.prototype.hasOwnProperty.call(
|
|
131
|
+
model.attributes,
|
|
132
|
+
arrAttr
|
|
133
|
+
)
|
|
134
|
+
) {
|
|
135
|
+
diagnostics.push(
|
|
136
|
+
lsp.Diagnostic.create(
|
|
137
|
+
lsp.Range.create(
|
|
138
|
+
document.positionAt(el.start),
|
|
139
|
+
document.positionAt(el.end)
|
|
140
|
+
),
|
|
141
|
+
`'${arrAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
142
|
+
lsp.DiagnosticSeverity.Error,
|
|
143
|
+
'sails-lsp'
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
if (attribute === 'sort' && prop.value) {
|
|
152
|
+
if (
|
|
153
|
+
prop.value.type === 'Literal' ||
|
|
154
|
+
prop.value.type === 'StringLiteral'
|
|
155
|
+
) {
|
|
156
|
+
const sortStr = prop.value.value
|
|
157
|
+
const sortAttr = sortStr && sortStr.split(' ')[0]
|
|
158
|
+
if (
|
|
159
|
+
sortAttr &&
|
|
160
|
+
(!model.attributes ||
|
|
161
|
+
!Object.prototype.hasOwnProperty.call(
|
|
162
|
+
model.attributes,
|
|
163
|
+
sortAttr
|
|
164
|
+
))
|
|
165
|
+
) {
|
|
166
|
+
diagnostics.push(
|
|
167
|
+
lsp.Diagnostic.create(
|
|
168
|
+
lsp.Range.create(
|
|
169
|
+
document.positionAt(prop.value.start),
|
|
170
|
+
document.positionAt(prop.value.end)
|
|
171
|
+
),
|
|
172
|
+
`'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
173
|
+
lsp.DiagnosticSeverity.Error,
|
|
174
|
+
'sails-lsp'
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
continue
|
|
179
|
+
} else if (prop.value.type === 'ArrayExpression') {
|
|
180
|
+
for (const el of prop.value.elements) {
|
|
181
|
+
if (!el) continue
|
|
182
|
+
if (el.type === 'ObjectExpression') {
|
|
183
|
+
for (const sortProp of el.properties) {
|
|
184
|
+
if (!sortProp.key) continue
|
|
185
|
+
const sortAttr =
|
|
186
|
+
sortProp.key.name || sortProp.key.value
|
|
187
|
+
if (
|
|
188
|
+
!model.attributes ||
|
|
189
|
+
!Object.prototype.hasOwnProperty.call(
|
|
190
|
+
model.attributes,
|
|
191
|
+
sortAttr
|
|
192
|
+
)
|
|
193
|
+
) {
|
|
194
|
+
diagnostics.push(
|
|
195
|
+
lsp.Diagnostic.create(
|
|
196
|
+
lsp.Range.create(
|
|
197
|
+
document.positionAt(sortProp.key.start),
|
|
198
|
+
document.positionAt(sortProp.key.end)
|
|
199
|
+
),
|
|
200
|
+
`'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
201
|
+
lsp.DiagnosticSeverity.Error,
|
|
202
|
+
'sails-lsp'
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else if (
|
|
208
|
+
el.type === 'Literal' ||
|
|
209
|
+
el.type === 'StringLiteral'
|
|
210
|
+
) {
|
|
211
|
+
const sortStr = el.value
|
|
212
|
+
const sortAttr = sortStr && sortStr.split(' ')[0]
|
|
213
|
+
if (
|
|
214
|
+
sortAttr &&
|
|
215
|
+
(!model.attributes ||
|
|
216
|
+
!Object.prototype.hasOwnProperty.call(
|
|
217
|
+
model.attributes,
|
|
218
|
+
sortAttr
|
|
219
|
+
))
|
|
220
|
+
) {
|
|
221
|
+
diagnostics.push(
|
|
222
|
+
lsp.Diagnostic.create(
|
|
223
|
+
lsp.Range.create(
|
|
224
|
+
document.positionAt(el.start),
|
|
225
|
+
document.positionAt(el.end)
|
|
226
|
+
),
|
|
227
|
+
`'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
228
|
+
lsp.DiagnosticSeverity.Error,
|
|
229
|
+
'sails-lsp'
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
continue
|
|
236
|
+
} else if (prop.value.type === 'ObjectExpression') {
|
|
237
|
+
for (const sortProp of prop.value.properties) {
|
|
238
|
+
if (!sortProp.key) continue
|
|
239
|
+
const sortAttr = sortProp.key.name || sortProp.key.value
|
|
240
|
+
if (
|
|
241
|
+
!model.attributes ||
|
|
242
|
+
!Object.prototype.hasOwnProperty.call(
|
|
243
|
+
model.attributes,
|
|
244
|
+
sortAttr
|
|
245
|
+
)
|
|
246
|
+
) {
|
|
247
|
+
diagnostics.push(
|
|
248
|
+
lsp.Diagnostic.create(
|
|
249
|
+
lsp.Range.create(
|
|
250
|
+
document.positionAt(sortProp.key.start),
|
|
251
|
+
document.positionAt(sortProp.key.end)
|
|
252
|
+
),
|
|
253
|
+
`'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
254
|
+
lsp.DiagnosticSeverity.Error,
|
|
255
|
+
'sails-lsp'
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// For all other query option keys, skip validation
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
266
|
+
// --- END ROBUST FIX ---
|
|
267
|
+
// Only validate top-level for create/createEach
|
|
268
|
+
if (
|
|
269
|
+
(method === 'create' || method === 'createEach') &&
|
|
270
|
+
(!model.attributes ||
|
|
271
|
+
!Object.prototype.hasOwnProperty.call(
|
|
272
|
+
model.attributes,
|
|
273
|
+
attribute
|
|
274
|
+
))
|
|
275
|
+
) {
|
|
276
|
+
diagnostics.push(
|
|
277
|
+
lsp.Diagnostic.create(
|
|
278
|
+
lsp.Range.create(
|
|
279
|
+
document.positionAt(prop.key.start),
|
|
280
|
+
document.positionAt(prop.key.end)
|
|
281
|
+
),
|
|
282
|
+
`'${attribute}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
283
|
+
lsp.DiagnosticSeverity.Error,
|
|
284
|
+
'sails-lsp'
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
} catch (err) {
|
|
294
|
+
// No regex fallback: rely solely on AST-based validation for accuracy
|
|
125
295
|
}
|
|
126
|
-
|
|
127
296
|
return diagnostics
|
|
128
297
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
/**
|
|
3
|
+
* Validate if a referenced model exists in Sails.js queries using regex.
|
|
4
|
+
* @param {TextDocument} document - The text document to validate.
|
|
5
|
+
* @param {Object} typeMap - The type map containing models.
|
|
6
|
+
* @returns {Array} diagnostics - Array of LSP diagnostics.
|
|
7
|
+
*/
|
|
8
|
+
module.exports = function validateModelExist(document, typeMap) {
|
|
9
|
+
const diagnostics = []
|
|
10
|
+
const text = document.getText()
|
|
11
|
+
// Build a lowercased model map for robust case-insensitive lookup
|
|
12
|
+
const modelMap = {}
|
|
13
|
+
if (typeMap.models) {
|
|
14
|
+
for (const key of Object.keys(typeMap.models)) {
|
|
15
|
+
modelMap[key.toLowerCase()] = typeMap.models[key]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function modelExists(name) {
|
|
19
|
+
if (!name) return false
|
|
20
|
+
return !!modelMap[name.toLowerCase()]
|
|
21
|
+
}
|
|
22
|
+
// User.find() or User.create() etc
|
|
23
|
+
const modelCallRegex =
|
|
24
|
+
/\b([A-Za-z0-9_]+)\s*\.(?:find|findOne|create|createEach|update|destroy|count|sum|where|findOrCreate)\s*\(/g
|
|
25
|
+
let match
|
|
26
|
+
while ((match = modelCallRegex.exec(text)) !== null) {
|
|
27
|
+
const modelName = match[1]
|
|
28
|
+
if (!modelExists(modelName)) {
|
|
29
|
+
diagnostics.push(
|
|
30
|
+
lsp.Diagnostic.create(
|
|
31
|
+
lsp.Range.create(
|
|
32
|
+
document.positionAt(match.index),
|
|
33
|
+
document.positionAt(match.index + modelName.length)
|
|
34
|
+
),
|
|
35
|
+
`Model '${modelName}' not found. Make sure it exists under your api/models directory.`,
|
|
36
|
+
lsp.DiagnosticSeverity.Error,
|
|
37
|
+
'sails-lsp'
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// sails.models.user.find() or sails.models.User.find()
|
|
43
|
+
const sailsModelCallRegex =
|
|
44
|
+
/sails\.models\.([A-Za-z0-9_]+)\s*\.(?:find|findOne|create|createEach|update|destroy|count|sum|where|findOrCreate)\s*\(/g
|
|
45
|
+
while ((match = sailsModelCallRegex.exec(text)) !== null) {
|
|
46
|
+
const modelName = match[1]
|
|
47
|
+
if (!modelExists(modelName)) {
|
|
48
|
+
diagnostics.push(
|
|
49
|
+
lsp.Diagnostic.create(
|
|
50
|
+
lsp.Range.create(
|
|
51
|
+
document.positionAt(match.index + 'sails.models.'.length),
|
|
52
|
+
document.positionAt(
|
|
53
|
+
match.index + 'sails.models.'.length + modelName.length
|
|
54
|
+
)
|
|
55
|
+
),
|
|
56
|
+
`Model '${modelName}' does not exist in this Sails project.`,
|
|
57
|
+
lsp.DiagnosticSeverity.Error,
|
|
58
|
+
'sails-lsp'
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return diagnostics
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
|
|
3
|
+
module.exports = function validateRequiredHelperInput(document, typeMap) {
|
|
4
|
+
const diagnostics = []
|
|
5
|
+
const text = document.getText()
|
|
6
|
+
|
|
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
|
+
)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return diagnostics
|
|
49
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
|
|
3
|
+
module.exports = function validateRequiredModelAttribute(document, typeMap) {
|
|
4
|
+
const diagnostics = []
|
|
5
|
+
const text = document.getText()
|
|
6
|
+
const models = typeMap.models || {}
|
|
7
|
+
|
|
8
|
+
// For each model, check for .create({ ... }) and .createEach({ ... }) calls
|
|
9
|
+
for (const [modelName, modelInfo] of Object.entries(models)) {
|
|
10
|
+
if (!modelInfo || !modelInfo.attributes) continue
|
|
11
|
+
// Only validate required attributes for create and createEach
|
|
12
|
+
const methodRegex = new RegExp(
|
|
13
|
+
`${modelName}\\s*\\.\\s*(create|createEach)\\s*\\(\\s*\\{([\\s\\S]*?)\\}\\s*\\)`,
|
|
14
|
+
'g'
|
|
15
|
+
)
|
|
16
|
+
let match
|
|
17
|
+
while ((match = methodRegex.exec(text)) !== null) {
|
|
18
|
+
// match[2] is the object literal content
|
|
19
|
+
// Find all property names in the object literal, including shorthand
|
|
20
|
+
const propsRegex = /([a-zA-Z0-9_]+)\s*:/g
|
|
21
|
+
const shorthandRegex = /([a-zA-Z0-9_]+)\s*(,|(?=\n|\}))/g
|
|
22
|
+
let propMatch
|
|
23
|
+
const providedKeys = new Set()
|
|
24
|
+
while ((propMatch = propsRegex.exec(match[2])) !== null) {
|
|
25
|
+
providedKeys.add(propMatch[1])
|
|
26
|
+
}
|
|
27
|
+
while ((propMatch = shorthandRegex.exec(match[2])) !== null) {
|
|
28
|
+
if (!providedKeys.has(propMatch[1])) {
|
|
29
|
+
providedKeys.add(propMatch[1])
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Check for missing required attributes
|
|
33
|
+
for (const [attr, def] of Object.entries(modelInfo.attributes)) {
|
|
34
|
+
const isRequired =
|
|
35
|
+
def && (def.required === true || def.required === 'true')
|
|
36
|
+
if (isRequired && !providedKeys.has(attr)) {
|
|
37
|
+
// Find the start/end of the object literal for the diagnostic range
|
|
38
|
+
const objStart = match.index + match[0].indexOf('{')
|
|
39
|
+
const objEnd = objStart + match[2].length + 1 // +1 for closing }
|
|
40
|
+
diagnostics.push(
|
|
41
|
+
lsp.Diagnostic.create(
|
|
42
|
+
lsp.Range.create(
|
|
43
|
+
document.positionAt(objStart),
|
|
44
|
+
document.positionAt(objEnd)
|
|
45
|
+
),
|
|
46
|
+
`Missing required attribute '${attr}' in ${modelName}.${match[1]}().`,
|
|
47
|
+
lsp.DiagnosticSeverity.Error,
|
|
48
|
+
'sails-lsp'
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return diagnostics
|
|
56
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
|
|
3
|
+
module.exports = function validateViewExist(document, typeMap) {
|
|
4
|
+
const diagnostics = []
|
|
5
|
+
|
|
6
|
+
// Only check routes.js and actions (api/controllers)
|
|
7
|
+
const isRoutes = document.uri.endsWith('routes.js')
|
|
8
|
+
const isAction = document.uri.includes('/api/controllers/')
|
|
9
|
+
if (!isRoutes && !isAction) return diagnostics
|
|
10
|
+
|
|
11
|
+
// Extract { view: '...' } in routes.js
|
|
12
|
+
if (isRoutes) {
|
|
13
|
+
const views = extractViewReferences(document)
|
|
14
|
+
for (const { view, range } of views) {
|
|
15
|
+
if (!typeMap.views?.[view]) {
|
|
16
|
+
diagnostics.push(
|
|
17
|
+
lsp.Diagnostic.create(
|
|
18
|
+
range,
|
|
19
|
+
`View '${view}' not found. Make sure it exists in your /views directory.`,
|
|
20
|
+
lsp.DiagnosticSeverity.Error,
|
|
21
|
+
'sails-lsp'
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Extract viewTemplatePath: '...' in actions
|
|
29
|
+
if (isAction) {
|
|
30
|
+
const exits = extractViewTemplatePathReferences(document)
|
|
31
|
+
for (const { view, range } of exits) {
|
|
32
|
+
if (!typeMap.views?.[view]) {
|
|
33
|
+
diagnostics.push(
|
|
34
|
+
lsp.Diagnostic.create(
|
|
35
|
+
range,
|
|
36
|
+
`View '${view}' not found. Make sure it exists in your /views directory.`,
|
|
37
|
+
lsp.DiagnosticSeverity.Error,
|
|
38
|
+
'sails-lsp'
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return diagnostics
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractViewReferences(document) {
|
|
49
|
+
const text = document.getText()
|
|
50
|
+
const regex = /view\s*:\s*['"]([^'"]+)['"]/g
|
|
51
|
+
const views = []
|
|
52
|
+
let match
|
|
53
|
+
while ((match = regex.exec(text)) !== null) {
|
|
54
|
+
const view = match[1]
|
|
55
|
+
const viewStart = match.index + match[0].indexOf(view)
|
|
56
|
+
const viewEnd = viewStart + view.length
|
|
57
|
+
views.push({
|
|
58
|
+
view,
|
|
59
|
+
range: lsp.Range.create(
|
|
60
|
+
document.positionAt(viewStart),
|
|
61
|
+
document.positionAt(viewEnd)
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
return views
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractViewTemplatePathReferences(document) {
|
|
69
|
+
const text = document.getText()
|
|
70
|
+
const regex = /viewTemplatePath\s*:\s*['"]([^'"]+)['"]/g
|
|
71
|
+
const views = []
|
|
72
|
+
let match
|
|
73
|
+
while ((match = regex.exec(text)) !== null) {
|
|
74
|
+
const view = match[1]
|
|
75
|
+
const viewStart = match.index + match[0].indexOf(view)
|
|
76
|
+
const viewEnd = viewStart + view.length
|
|
77
|
+
views.push({
|
|
78
|
+
view,
|
|
79
|
+
range: lsp.Range.create(
|
|
80
|
+
document.positionAt(viewStart),
|
|
81
|
+
document.positionAt(viewEnd)
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
return views
|
|
86
|
+
}
|