biome-plugin-suitescript 0.1.1 → 0.3.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.
Files changed (78) hide show
  1. package/README.md +157 -27
  2. package/biome-manifest.json +261 -7
  3. package/configs/all.json +29 -0
  4. package/configs/minimal.json +14 -0
  5. package/configs/recommended.json +20 -0
  6. package/dist/cli/analyze.js +33 -3
  7. package/dist/lib/define.js +35 -5
  8. package/dist/lib/headers.js +12 -0
  9. package/dist/lib/module-calls.js +65 -0
  10. package/dist/lib/module-script-types.js +76 -0
  11. package/dist/lib/options.js +35 -0
  12. package/dist/lib/required-options.js +14 -0
  13. package/dist/lib/script-side.js +83 -0
  14. package/dist/lib/script-types.js +37 -14
  15. package/dist/lib/suiteql.js +16 -0
  16. package/dist/lib/suitescript.js +23 -0
  17. package/dist/lib/typings-data.js +4327 -0
  18. package/dist/lib/typings.js +2 -0
  19. package/dist/rules/logRequireDetails.js +4 -0
  20. package/dist/rules/logRequireTitle.js +1 -0
  21. package/dist/rules/noAuthorizationHeaderInInternalRequest.js +24 -0
  22. package/dist/rules/noBeforeSubmitRecordReload.js +19 -0
  23. package/dist/rules/noBlockedHttpsHeader.js +32 -0
  24. package/dist/rules/noBlockedResponseHeader.js +35 -0
  25. package/dist/rules/noBrowserGlobals.js +25 -0
  26. package/dist/rules/noClientLog.js +14 -0
  27. package/dist/rules/noConflictingClientScriptProps.js +36 -0
  28. package/dist/rules/noDynamicRequire.js +23 -0
  29. package/dist/rules/noDynamicSuiteQL.js +29 -0
  30. package/dist/rules/noEmptyEntryPoints.js +19 -0
  31. package/dist/rules/noExtraDeps.js +16 -0
  32. package/dist/rules/noExtraParams.js +17 -0
  33. package/dist/rules/noGlobalShadowing.js +38 -0
  34. package/dist/rules/noHardcodedAuthPasswords.js +27 -0
  35. package/dist/rules/noHardcodedSecrets.js +43 -0
  36. package/dist/rules/noHeaderUnderscores.js +23 -0
  37. package/dist/rules/noInnerHtml.js +41 -0
  38. package/dist/rules/noInsecureCookie.js +27 -0
  39. package/dist/rules/noInsecureHttpsUrl.js +20 -0
  40. package/dist/rules/noInvalidDefine.js +47 -0
  41. package/dist/rules/noInvalidEnumMember.js +24 -0
  42. package/dist/rules/noLocationHeader.js +20 -0
  43. package/dist/rules/noLogModule.js +8 -2
  44. package/dist/rules/noMapReduceSearchResults.js +33 -0
  45. package/dist/rules/noMissingModules.js +29 -0
  46. package/dist/rules/noModuleScopeApiCalls.js +49 -0
  47. package/dist/rules/noMultipleDefine.js +14 -0
  48. package/dist/rules/noNativeIterators.js +19 -0
  49. package/dist/rules/noPositionalOptions.js +26 -0
  50. package/dist/rules/noPostMessageWildcard.js +15 -0
  51. package/dist/rules/noPublicFileUpload.js +20 -0
  52. package/dist/rules/noQueryColumnFieldAndFormula.js +19 -0
  53. package/dist/rules/noRecordInLoop.js +34 -0
  54. package/dist/rules/noRequireInsteadOfDefine.js +25 -0
  55. package/dist/rules/noSecretKeyGuidAndSecret.js +16 -0
  56. package/dist/rules/noSensitiveBrowserStorage.js +24 -0
  57. package/dist/rules/noStackInResponse.js +21 -0
  58. package/dist/rules/noSuiteQLSelectStar.js +18 -0
  59. package/dist/rules/noSuiteQLSystemNoteJoin.js +19 -0
  60. package/dist/rules/noSuiteQLTypeInSearch.js +41 -0
  61. package/dist/rules/noSuiteScript1Api.js +84 -0
  62. package/dist/rules/noUnknownModuleMember.js +23 -0
  63. package/dist/rules/noUnknownOptions.js +31 -0
  64. package/dist/rules/noUnsupportedServerWidgetFieldType.js +40 -0
  65. package/dist/rules/noWeakHash.js +17 -0
  66. package/dist/rules/noWildcardCors.js +16 -0
  67. package/dist/rules/noWrongScriptTypeModules.js +30 -0
  68. package/dist/rules/requireClientValidateReturn.js +27 -0
  69. package/dist/rules/requireCustpagePrefix.js +23 -0
  70. package/dist/rules/requireEachReturn.js +38 -0
  71. package/dist/rules/requireLogModule.js +25 -0
  72. package/dist/rules/requireOptions.js +26 -0
  73. package/dist/rules/requireOrderByForPagedSuiteQL.js +17 -0
  74. package/dist/rules/requireQueryOptions.js +53 -0
  75. package/dist/rules/requireServerResponseOptions.js +33 -0
  76. package/dist/rules/requireServerWidgetOptions.js +37 -0
  77. package/package.json +3 -1
  78. package/dist/rules/noExtraModules.js +0 -23
@@ -3,30 +3,122 @@
3
3
  "configs": [
4
4
  {
5
5
  "suitescript2": "./configs/suitescript2.json"
6
+ },
7
+ {
8
+ "minimal": "./configs/minimal.json"
9
+ },
10
+ {
11
+ "recommended": "./configs/recommended.json"
12
+ },
13
+ {
14
+ "all": "./configs/all.json"
6
15
  }
7
16
  ],
8
17
  "plugins": {
9
18
  "presets": {
10
19
  "all": [
11
20
  "noAmdName",
12
- "noExtraModules",
21
+ "noExtraDeps",
22
+ "noExtraParams",
23
+ "noInvalidDefine",
13
24
  "noInvalidModules",
14
25
  "noModuleExtensions",
26
+ "noMultipleDefine",
27
+ "noRequireInsteadOfDefine",
28
+ "noDynamicRequire",
15
29
  "moduleVars",
16
30
  "logRequireTitle",
31
+ "entryPoints",
32
+ "noMissingModules",
33
+ "noGlobalShadowing",
34
+ "noDynamicSuiteQL",
35
+ "noSuiteScript1Api",
36
+ "noModuleScopeApiCalls",
37
+ "noNativeIterators",
38
+ "noWrongScriptTypeModules",
39
+ "noBrowserGlobals",
40
+ "noInsecureHttpsUrl",
41
+ "noBeforeSubmitRecordReload",
42
+ "noEmptyEntryPoints",
43
+ "requireCustpagePrefix",
44
+ "noHeaderUnderscores",
45
+ "noQueryColumnFieldAndFormula",
46
+ "requireClientValidateReturn",
47
+ "noPostMessageWildcard",
48
+ "noInnerHtml",
49
+ "noWildcardCors",
50
+ "noSuiteQLTypeInSearch",
51
+ "requireEachReturn",
52
+ "noAuthorizationHeaderInInternalRequest",
53
+ "noBlockedResponseHeader",
54
+ "noBlockedHttpsHeader",
55
+ "requireOptions",
56
+ "noPositionalOptions",
57
+ "noUnknownModuleMember",
58
+ "requireServerWidgetOptions",
59
+ "requireQueryOptions",
60
+ "requireServerResponseOptions",
61
+ "noConflictingClientScriptProps",
62
+ "noUnsupportedServerWidgetFieldType",
63
+ "noHardcodedAuthPasswords",
64
+ "noSecretKeyGuidAndSecret",
65
+ "requireOrderByForPagedSuiteQL",
17
66
  "logRequireDetails",
18
- "noLogModule",
19
- "entryPoints"
67
+ "noUnknownOptions",
68
+ "noInvalidEnumMember",
69
+ "requireLogModule",
70
+ "noRecordInLoop",
71
+ "noMapReduceSearchResults",
72
+ "noClientLog"
20
73
  ],
21
- "minimal": ["noExtraModules", "noInvalidModules"],
74
+ "minimal": ["noExtraDeps", "noExtraParams", "noInvalidModules"],
22
75
  "recommended": [
23
76
  "noAmdName",
24
- "noExtraModules",
77
+ "noExtraDeps",
78
+ "noExtraParams",
79
+ "noInvalidDefine",
25
80
  "noInvalidModules",
26
81
  "noModuleExtensions",
82
+ "noMultipleDefine",
83
+ "noRequireInsteadOfDefine",
84
+ "noDynamicRequire",
27
85
  "moduleVars",
28
86
  "logRequireTitle",
29
- "logRequireDetails"
87
+ "entryPoints",
88
+ "noMissingModules",
89
+ "noGlobalShadowing",
90
+ "noDynamicSuiteQL",
91
+ "noSuiteScript1Api",
92
+ "noModuleScopeApiCalls",
93
+ "noNativeIterators",
94
+ "noWrongScriptTypeModules",
95
+ "noBrowserGlobals",
96
+ "noInsecureHttpsUrl",
97
+ "noBeforeSubmitRecordReload",
98
+ "noEmptyEntryPoints",
99
+ "requireCustpagePrefix",
100
+ "noHeaderUnderscores",
101
+ "noQueryColumnFieldAndFormula",
102
+ "requireClientValidateReturn",
103
+ "noPostMessageWildcard",
104
+ "noInnerHtml",
105
+ "noWildcardCors",
106
+ "noSuiteQLTypeInSearch",
107
+ "requireEachReturn",
108
+ "noAuthorizationHeaderInInternalRequest",
109
+ "noBlockedResponseHeader",
110
+ "noBlockedHttpsHeader",
111
+ "requireOptions",
112
+ "noPositionalOptions",
113
+ "noUnknownModuleMember",
114
+ "requireServerWidgetOptions",
115
+ "requireQueryOptions",
116
+ "requireServerResponseOptions",
117
+ "noConflictingClientScriptProps",
118
+ "noUnsupportedServerWidgetFieldType",
119
+ "noHardcodedAuthPasswords",
120
+ "noSecretKeyGuidAndSecret",
121
+ "requireOrderByForPagedSuiteQL"
30
122
  ]
31
123
  },
32
124
  "rules": [
@@ -46,16 +138,178 @@
46
138
  "noAmdName": "./dist/rules/noAmdName.grit"
47
139
  },
48
140
  {
49
- "noExtraModules": "./dist/rules/noExtraModules.js"
141
+ "noAuthorizationHeaderInInternalRequest": "./dist/rules/noAuthorizationHeaderInInternalRequest.js"
142
+ },
143
+ {
144
+ "noBeforeSubmitRecordReload": "./dist/rules/noBeforeSubmitRecordReload.js"
145
+ },
146
+ {
147
+ "noBlockedHttpsHeader": "./dist/rules/noBlockedHttpsHeader.js"
148
+ },
149
+ {
150
+ "noBlockedResponseHeader": "./dist/rules/noBlockedResponseHeader.js"
151
+ },
152
+ {
153
+ "noBrowserGlobals": "./dist/rules/noBrowserGlobals.js"
154
+ },
155
+ {
156
+ "noClientLog": "./dist/rules/noClientLog.js"
157
+ },
158
+ {
159
+ "noConflictingClientScriptProps": "./dist/rules/noConflictingClientScriptProps.js"
160
+ },
161
+ {
162
+ "noDynamicRequire": "./dist/rules/noDynamicRequire.js"
163
+ },
164
+ {
165
+ "noDynamicSuiteQL": "./dist/rules/noDynamicSuiteQL.js"
166
+ },
167
+ {
168
+ "noEmptyEntryPoints": "./dist/rules/noEmptyEntryPoints.js"
169
+ },
170
+ {
171
+ "noExtraDeps": "./dist/rules/noExtraDeps.js"
172
+ },
173
+ {
174
+ "noExtraParams": "./dist/rules/noExtraParams.js"
175
+ },
176
+ {
177
+ "noGlobalShadowing": "./dist/rules/noGlobalShadowing.js"
178
+ },
179
+ {
180
+ "noHardcodedAuthPasswords": "./dist/rules/noHardcodedAuthPasswords.js"
181
+ },
182
+ {
183
+ "noHardcodedSecrets": "./dist/rules/noHardcodedSecrets.js"
184
+ },
185
+ {
186
+ "noHeaderUnderscores": "./dist/rules/noHeaderUnderscores.js"
187
+ },
188
+ {
189
+ "noInnerHtml": "./dist/rules/noInnerHtml.js"
190
+ },
191
+ {
192
+ "noInsecureCookie": "./dist/rules/noInsecureCookie.js"
193
+ },
194
+ {
195
+ "noInsecureHttpsUrl": "./dist/rules/noInsecureHttpsUrl.js"
196
+ },
197
+ {
198
+ "noInvalidDefine": "./dist/rules/noInvalidDefine.js"
199
+ },
200
+ {
201
+ "noInvalidEnumMember": "./dist/rules/noInvalidEnumMember.js"
50
202
  },
51
203
  {
52
204
  "noInvalidModules": "./dist/rules/noInvalidModules.js"
53
205
  },
206
+ {
207
+ "noLocationHeader": "./dist/rules/noLocationHeader.js"
208
+ },
54
209
  {
55
210
  "noLogModule": "./dist/rules/noLogModule.js"
56
211
  },
212
+ {
213
+ "noMapReduceSearchResults": "./dist/rules/noMapReduceSearchResults.js"
214
+ },
215
+ {
216
+ "noMissingModules": "./dist/rules/noMissingModules.js"
217
+ },
57
218
  {
58
219
  "noModuleExtensions": "./dist/rules/noModuleExtensions.grit"
220
+ },
221
+ {
222
+ "noModuleScopeApiCalls": "./dist/rules/noModuleScopeApiCalls.js"
223
+ },
224
+ {
225
+ "noMultipleDefine": "./dist/rules/noMultipleDefine.js"
226
+ },
227
+ {
228
+ "noNativeIterators": "./dist/rules/noNativeIterators.js"
229
+ },
230
+ {
231
+ "noPositionalOptions": "./dist/rules/noPositionalOptions.js"
232
+ },
233
+ {
234
+ "noPostMessageWildcard": "./dist/rules/noPostMessageWildcard.js"
235
+ },
236
+ {
237
+ "noPublicFileUpload": "./dist/rules/noPublicFileUpload.js"
238
+ },
239
+ {
240
+ "noQueryColumnFieldAndFormula": "./dist/rules/noQueryColumnFieldAndFormula.js"
241
+ },
242
+ {
243
+ "noRecordInLoop": "./dist/rules/noRecordInLoop.js"
244
+ },
245
+ {
246
+ "noRequireInsteadOfDefine": "./dist/rules/noRequireInsteadOfDefine.js"
247
+ },
248
+ {
249
+ "noSecretKeyGuidAndSecret": "./dist/rules/noSecretKeyGuidAndSecret.js"
250
+ },
251
+ {
252
+ "noSensitiveBrowserStorage": "./dist/rules/noSensitiveBrowserStorage.js"
253
+ },
254
+ {
255
+ "noStackInResponse": "./dist/rules/noStackInResponse.js"
256
+ },
257
+ {
258
+ "noSuiteQLSelectStar": "./dist/rules/noSuiteQLSelectStar.js"
259
+ },
260
+ {
261
+ "noSuiteQLSystemNoteJoin": "./dist/rules/noSuiteQLSystemNoteJoin.js"
262
+ },
263
+ {
264
+ "noSuiteQLTypeInSearch": "./dist/rules/noSuiteQLTypeInSearch.js"
265
+ },
266
+ {
267
+ "noSuiteScript1Api": "./dist/rules/noSuiteScript1Api.js"
268
+ },
269
+ {
270
+ "noUnknownModuleMember": "./dist/rules/noUnknownModuleMember.js"
271
+ },
272
+ {
273
+ "noUnknownOptions": "./dist/rules/noUnknownOptions.js"
274
+ },
275
+ {
276
+ "noUnsupportedServerWidgetFieldType": "./dist/rules/noUnsupportedServerWidgetFieldType.js"
277
+ },
278
+ {
279
+ "noWeakHash": "./dist/rules/noWeakHash.js"
280
+ },
281
+ {
282
+ "noWildcardCors": "./dist/rules/noWildcardCors.js"
283
+ },
284
+ {
285
+ "noWrongScriptTypeModules": "./dist/rules/noWrongScriptTypeModules.js"
286
+ },
287
+ {
288
+ "requireClientValidateReturn": "./dist/rules/requireClientValidateReturn.js"
289
+ },
290
+ {
291
+ "requireCustpagePrefix": "./dist/rules/requireCustpagePrefix.js"
292
+ },
293
+ {
294
+ "requireEachReturn": "./dist/rules/requireEachReturn.js"
295
+ },
296
+ {
297
+ "requireLogModule": "./dist/rules/requireLogModule.js"
298
+ },
299
+ {
300
+ "requireOptions": "./dist/rules/requireOptions.js"
301
+ },
302
+ {
303
+ "requireOrderByForPagedSuiteQL": "./dist/rules/requireOrderByForPagedSuiteQL.js"
304
+ },
305
+ {
306
+ "requireQueryOptions": "./dist/rules/requireQueryOptions.js"
307
+ },
308
+ {
309
+ "requireServerResponseOptions": "./dist/rules/requireServerResponseOptions.js"
310
+ },
311
+ {
312
+ "requireServerWidgetOptions": "./dist/rules/requireServerWidgetOptions.js"
59
313
  }
60
314
  ]
61
315
  },
@@ -0,0 +1,29 @@
1
+ {
2
+ "javascript": {
3
+ "globals": ["define", "require", "log", "util"]
4
+ },
5
+ "linter": {
6
+ "rules": {
7
+ "correctness": {
8
+ "noUnusedFunctionParameters": "off"
9
+ },
10
+ "nursery": {
11
+ "noImpliedEval": "error",
12
+ "useIncludes": "warn"
13
+ },
14
+ "preset": "recommended",
15
+ "security": {
16
+ "noGlobalEval": "error"
17
+ },
18
+ "style": {
19
+ "useConst": "warn",
20
+ "useForOf": "warn",
21
+ "useTemplate": "warn"
22
+ },
23
+ "suspicious": {
24
+ "noVar": "warn"
25
+ }
26
+ }
27
+ },
28
+ "plugins": ["biome-plugin-suitescript/presets/all"]
29
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "javascript": {
3
+ "globals": ["define", "require", "log", "util"]
4
+ },
5
+ "linter": {
6
+ "rules": {
7
+ "correctness": {
8
+ "noUnusedFunctionParameters": "off"
9
+ },
10
+ "preset": "recommended"
11
+ }
12
+ },
13
+ "plugins": ["biome-plugin-suitescript/presets/minimal"]
14
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "javascript": {
3
+ "globals": ["define", "require", "log", "util"]
4
+ },
5
+ "linter": {
6
+ "rules": {
7
+ "correctness": {
8
+ "noUnusedFunctionParameters": "off"
9
+ },
10
+ "nursery": {
11
+ "noImpliedEval": "error"
12
+ },
13
+ "preset": "recommended",
14
+ "security": {
15
+ "noGlobalEval": "error"
16
+ }
17
+ }
18
+ },
19
+ "plugins": ["biome-plugin-suitescript/presets/recommended"]
20
+ }
@@ -1,6 +1,7 @@
1
1
  // Node-only analysis of a single SuiteScript source file (ESTree via oxc-parser).
2
2
  import { parseSync, } from "oxc-parser";
3
3
  import { declaredProblems, inferredProblems, inferScriptType, typeByName, } from "../lib/inference.js";
4
+ import { misplacedModule, sideOf, unsupportedModule, } from "../lib/module-script-types.js";
4
5
  const API_VERSION = "2.1";
5
6
  const UPGRADABLE = new Set(["1.0", "2.0", "2.x"]);
6
7
  function isCallback(node) {
@@ -97,6 +98,7 @@ function exportedNames(node) {
97
98
  }
98
99
  function inspectProgram(program) {
99
100
  let isScript = false;
101
+ const modules = [];
100
102
  const returned = [];
101
103
  let opaque = false;
102
104
  // Only trust `returned` when an inline define() callback or ES syntax was seen;
@@ -106,8 +108,11 @@ function inspectProgram(program) {
106
108
  if (node.type === "ImportDeclaration" ||
107
109
  node.type === "ExportNamedDeclaration")
108
110
  known = true;
109
- if (node.type === "ImportDeclaration" && /^N(\/|$)/.test(node.source.value))
111
+ if (node.type === "ImportDeclaration" &&
112
+ /^N(\/|$)/.test(node.source.value)) {
110
113
  isScript = true;
114
+ modules.push(node.source.value);
115
+ }
111
116
  if (node.type === "ExportNamedDeclaration")
112
117
  returned.push(...exportedNames(node));
113
118
  if (node.type === "ExpressionStatement" &&
@@ -115,6 +120,14 @@ function inspectProgram(program) {
115
120
  const call = node.expression;
116
121
  if (call.callee.type === "Identifier" && call.callee.name === "define") {
117
122
  isScript = true;
123
+ const dependencies = call.arguments.at(-2);
124
+ if (dependencies?.type === "ArrayExpression") {
125
+ for (const element of dependencies.elements) {
126
+ if (element?.type === "Literal" &&
127
+ typeof element.value === "string")
128
+ modules.push(element.value);
129
+ }
130
+ }
118
131
  const callback = call.arguments.at(-1);
119
132
  if (isCallback(callback)) {
120
133
  const names = amdReturnedNames(callback);
@@ -132,7 +145,11 @@ function inspectProgram(program) {
132
145
  }
133
146
  }
134
147
  }
135
- return { isScript, returned: opaque || !known ? undefined : returned };
148
+ return {
149
+ isScript,
150
+ modules,
151
+ returned: opaque || !known ? undefined : returned,
152
+ };
136
153
  }
137
154
  function findTag(comments, name) {
138
155
  const pattern = new RegExp(`${name}(?:[ \\t]+(\\S+))?`);
@@ -182,7 +199,7 @@ export function analyzeSource(filename, source) {
182
199
  }
183
200
  const apiVersion = findTag(parsed.comments, "@NApiVersion");
184
201
  const scriptType = findTag(parsed.comments, "@NScriptType");
185
- const { isScript, returned } = inspectProgram(parsed.program);
202
+ const { isScript, modules, returned } = inspectProgram(parsed.program);
186
203
  const inferred = isScript && returned ? inferScriptType(returned) : undefined;
187
204
  // Not a script: no tags and nothing that implies an entry point.
188
205
  if (!apiVersion && !scriptType && !inferred)
@@ -223,5 +240,18 @@ export function analyzeSource(filename, source) {
223
240
  for (const message of problems)
224
241
  report("entryPoints", message, scriptType?.offset ?? 0);
225
242
  }
243
+ // The declared type wins; otherwise fall back to what the entry points imply.
244
+ const declaredType = declared ? typeByName(declared) : undefined;
245
+ const side = sideOf(declaredType ? [declaredType] : (inferred?.types ?? []));
246
+ const single = declaredType ??
247
+ (inferred?.types.length === 1 ? inferred.types[0] : undefined);
248
+ if (side || single) {
249
+ for (const module of modules) {
250
+ const message = (side && misplacedModule(side, module)) ||
251
+ (single && unsupportedModule(single.name, module));
252
+ if (message)
253
+ report("modules", message, scriptType?.offset ?? 0);
254
+ }
255
+ }
226
256
  return { diagnostics, upgrade };
227
257
  }
@@ -8,11 +8,38 @@ export function stringValue(node) {
8
8
  return undefined;
9
9
  return node.text.slice(1, -1);
10
10
  }
11
+ /**
12
+ * The array literal a variable is initialised with, for `var dependencies = [...]`
13
+ * followed by `define(dependencies, ...)`. The first matching declaration in the
14
+ * file wins; a variable that is not an array literal is not resolved.
15
+ */
16
+ function declaredArray(from, name) {
17
+ const root = [...from.ancestors()].at(-1);
18
+ const visit = (node) => {
19
+ if (node.kind === "JS_VARIABLE_DECLARATOR") {
20
+ const children = node.children();
21
+ const id = children.find((c) => c.kind === "JS_IDENTIFIER_BINDING");
22
+ const init = children.find((c) => c.kind === "JS_INITIALIZER_CLAUSE");
23
+ const value = init?.children()[0];
24
+ if (id?.text === name && value?.kind === "JS_ARRAY_EXPRESSION") {
25
+ return value;
26
+ }
27
+ }
28
+ for (const child of node.children()) {
29
+ const found = visit(child);
30
+ if (found)
31
+ return found;
32
+ }
33
+ return undefined;
34
+ };
35
+ return root ? visit(root) : undefined;
36
+ }
11
37
  /**
12
38
  * Parses an AMD `define([...], function (...) {})` call.
13
39
  *
14
- * Returns undefined unless `call` is `define(...)` with a dependency array
15
- * followed by a function callback (the optional leading module id is allowed).
40
+ * Returns undefined unless `call` is `define(...)` with a dependency array (a
41
+ * literal, or a variable initialised with one) followed by a function callback (the
42
+ * optional leading module id is allowed).
16
43
  */
17
44
  export function parseDefine(call) {
18
45
  if (call.callee?.text !== "define")
@@ -20,11 +47,14 @@ export function parseDefine(call) {
20
47
  const args = call.arguments?.children()[0]?.children() ?? [];
21
48
  if (args.length < 2 || args.length > 3)
22
49
  return undefined;
23
- const deps = args[args.length - 2];
50
+ const given = args[args.length - 2];
24
51
  const callback = args[args.length - 1];
25
- if (!deps || !callback)
52
+ if (!given || !callback)
26
53
  return undefined;
27
- if (deps.kind !== "JS_ARRAY_EXPRESSION")
54
+ const deps = given.kind === "JS_IDENTIFIER_EXPRESSION"
55
+ ? declaredArray(call, given.text)
56
+ : given;
57
+ if (deps?.kind !== "JS_ARRAY_EXPRESSION")
28
58
  return undefined;
29
59
  if (!FUNCTION_KINDS.includes(callback.kind))
30
60
  return undefined;
@@ -0,0 +1,12 @@
1
+ import { optionsObject, propertyValue, stringValue } from "./options.js";
2
+ /** The header name and value nodes of `x.setHeader({ name, value })` or `addHeader`. */
3
+ export function headerCall(call) {
4
+ if (!/\.(setHeader|addHeader)$/.test(call.callee?.text ?? ""))
5
+ return undefined;
6
+ const options = optionsObject(call);
7
+ const nameNode = options && propertyValue(options, "name");
8
+ const name = stringValue(nameNode)?.toLowerCase();
9
+ if (!options || !nameNode || !name)
10
+ return undefined;
11
+ return { name, nameNode, value: propertyValue(options, "value") };
12
+ }
@@ -0,0 +1,65 @@
1
+ import { dependencies } from "./biome-modules.js";
2
+ import { parseDefine, stringValue } from "./define.js";
3
+ import { isFunction } from "./script-side.js";
4
+ import { TYPINGS } from "./typings-data.js";
5
+ const has = (object, key) => Object.hasOwn(object, key);
6
+ /**
7
+ * The N/ module a receiver expression (`record` in `record.load`) is bound to by a
8
+ * define() callback parameter or an import. A local variable or parameter of the
9
+ * same name shadows it, and a global resolves to nothing.
10
+ */
11
+ export function moduleOfReceiver(receiver, model) {
12
+ if (receiver.kind !== "JS_IDENTIFIER_EXPRESSION")
13
+ return undefined;
14
+ const reference = receiver.children()[0];
15
+ if (reference?.kind !== "JS_REFERENCE_IDENTIFIER")
16
+ return undefined;
17
+ const declaration = model.binding(reference)?.syntax();
18
+ if (!declaration)
19
+ return undefined;
20
+ const nearestFunction = declaration.ancestors().find(isFunction);
21
+ for (const ancestor of declaration.ancestors()) {
22
+ if (ancestor.kind === "JS_IMPORT") {
23
+ return dependencies(ancestor).find((d) => d.param?.text === declaration.text)?.name;
24
+ }
25
+ if (ancestor.kind === "JS_CALL_EXPRESSION") {
26
+ const def = parseDefine(ancestor);
27
+ if (def && nearestFunction?.text === def.callback.text) {
28
+ const index = def.params.findIndex((p) => p?.text === declaration.text);
29
+ return index >= 0 ? stringValue(def.elements[index]) : undefined;
30
+ }
31
+ }
32
+ }
33
+ return undefined;
34
+ }
35
+ /** The generated data for a module, if there is any. */
36
+ export function typingsOf(module) {
37
+ return module && has(TYPINGS, module) ? TYPINGS[module] : undefined;
38
+ }
39
+ /** A call to a function of a loaded N/ module that the type definitions describe. */
40
+ export function moduleCall(call, model) {
41
+ let callee = call.callee;
42
+ if (callee?.kind !== "JS_STATIC_MEMBER_EXPRESSION")
43
+ return undefined;
44
+ if (callee.children()[1]?.text === "promise") {
45
+ const inner = callee.children()[0];
46
+ if (inner?.kind !== "JS_STATIC_MEMBER_EXPRESSION")
47
+ return undefined;
48
+ callee = inner;
49
+ }
50
+ const [object, member] = callee.children();
51
+ if (!object || !member)
52
+ return undefined;
53
+ const module = moduleOfReceiver(object, model);
54
+ const typings = typingsOf(module);
55
+ if (!module || !typings || !has(typings.functions, member.text)) {
56
+ return undefined;
57
+ }
58
+ return {
59
+ alternatives: typings.functions[member.text],
60
+ callee: callee.text,
61
+ fn: member.text,
62
+ module,
63
+ typings,
64
+ };
65
+ }
@@ -0,0 +1,76 @@
1
+ // Which side of NetSuite a script type runs on, and the N/ modules that only
2
+ // work on one side. From the "Supported Script Types" column of Oracle's SuiteScript
3
+ // module reference; loading a module on the wrong side fails at runtime.
4
+ // N/ui/message is not listed although the table marks it client-only. No agent
5
+ // skill requires it to be client-only: the SAFE guide's complete Suitelet sample
6
+ // (references/appendices/appendix-ncache-sample.md) loads it and passes
7
+ // message.Type to Form.addPageInitMessage. The type definitions accept a message
8
+ // there too (N/ui/serverWidget.d.ts). And Oracle's Form.addPageInitMessage page
9
+ // (Suitelets and user event beforeLoad) and FAQ sample build the message on the
10
+ // server. Only show() and hide() need a browser.
11
+ // Pure logic shared by the Biome rules and the CLI.
12
+ export const CLIENT_ONLY_MODULES = new Set([
13
+ "N/currentRecord",
14
+ "N/portlet",
15
+ "N/ui/dialog",
16
+ ]);
17
+ export const SERVER_ONLY_MODULES = new Set([
18
+ "N/auth",
19
+ "N/cache",
20
+ "N/certificateControl",
21
+ "N/compress",
22
+ "N/config",
23
+ "N/crypto",
24
+ "N/crypto/certificate",
25
+ "N/dataset",
26
+ "N/documentCapture",
27
+ "N/encode",
28
+ "N/error",
29
+ "N/file",
30
+ "N/https/clientCertificate",
31
+ "N/keyControl",
32
+ "N/llm",
33
+ "N/machineTranslation",
34
+ "N/pgp",
35
+ "N/piremoval",
36
+ "N/plugin",
37
+ "N/redirect",
38
+ "N/render",
39
+ "N/sftp",
40
+ "N/task",
41
+ "N/task/accounting/recognition",
42
+ "N/ui/serverWidget",
43
+ "N/workbook",
44
+ "N/workflow",
45
+ ]);
46
+ /** The side shared by every candidate script type, or undefined when unknown or mixed. */
47
+ export function sideOf(types) {
48
+ if (types.length === 0)
49
+ return undefined;
50
+ const clients = types.filter((t) => t.name === "ClientScript").length;
51
+ if (clients === types.length)
52
+ return "client";
53
+ return clients === 0 ? "server" : undefined;
54
+ }
55
+ /** A message when `module` cannot be loaded by a script on `side`. */
56
+ export function misplacedModule(side, module) {
57
+ if (side === "server" && CLIENT_ONLY_MODULES.has(module))
58
+ return `${module} is only available in client scripts.`;
59
+ if (side === "client" && SERVER_ONLY_MODULES.has(module))
60
+ return `${module} is only available in server scripts.`;
61
+ return undefined;
62
+ }
63
+ const CUSTOM_TOOL_UNSUPPORTED = new Set([
64
+ "N/http",
65
+ "N/https",
66
+ "N/llm",
67
+ "N/sftp",
68
+ ]);
69
+ /** A message when a single script type cannot load `module` at all. */
70
+ export function unsupportedModule(scriptType, module) {
71
+ if (scriptType === "CustomTool" && CUSTOM_TOOL_UNSUPPORTED.has(module))
72
+ return `${module} is not supported in custom tool scripts.`;
73
+ if (scriptType === "BundleInstallationScript" && !/^N(\/|$)/.test(module))
74
+ return "Custom modules are not supported in bundle installation scripts.";
75
+ return undefined;
76
+ }