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.
- package/README.md +157 -27
- package/biome-manifest.json +261 -7
- package/configs/all.json +29 -0
- package/configs/minimal.json +14 -0
- package/configs/recommended.json +20 -0
- package/dist/cli/analyze.js +33 -3
- package/dist/lib/define.js +35 -5
- package/dist/lib/headers.js +12 -0
- package/dist/lib/module-calls.js +65 -0
- package/dist/lib/module-script-types.js +76 -0
- package/dist/lib/options.js +35 -0
- package/dist/lib/required-options.js +14 -0
- package/dist/lib/script-side.js +83 -0
- package/dist/lib/script-types.js +37 -14
- package/dist/lib/suiteql.js +16 -0
- package/dist/lib/suitescript.js +23 -0
- package/dist/lib/typings-data.js +4327 -0
- package/dist/lib/typings.js +2 -0
- package/dist/rules/logRequireDetails.js +4 -0
- package/dist/rules/logRequireTitle.js +1 -0
- package/dist/rules/noAuthorizationHeaderInInternalRequest.js +24 -0
- package/dist/rules/noBeforeSubmitRecordReload.js +19 -0
- package/dist/rules/noBlockedHttpsHeader.js +32 -0
- package/dist/rules/noBlockedResponseHeader.js +35 -0
- package/dist/rules/noBrowserGlobals.js +25 -0
- package/dist/rules/noClientLog.js +14 -0
- package/dist/rules/noConflictingClientScriptProps.js +36 -0
- package/dist/rules/noDynamicRequire.js +23 -0
- package/dist/rules/noDynamicSuiteQL.js +29 -0
- package/dist/rules/noEmptyEntryPoints.js +19 -0
- package/dist/rules/noExtraDeps.js +16 -0
- package/dist/rules/noExtraParams.js +17 -0
- package/dist/rules/noGlobalShadowing.js +38 -0
- package/dist/rules/noHardcodedAuthPasswords.js +27 -0
- package/dist/rules/noHardcodedSecrets.js +43 -0
- package/dist/rules/noHeaderUnderscores.js +23 -0
- package/dist/rules/noInnerHtml.js +41 -0
- package/dist/rules/noInsecureCookie.js +27 -0
- package/dist/rules/noInsecureHttpsUrl.js +20 -0
- package/dist/rules/noInvalidDefine.js +47 -0
- package/dist/rules/noInvalidEnumMember.js +24 -0
- package/dist/rules/noLocationHeader.js +20 -0
- package/dist/rules/noLogModule.js +8 -2
- package/dist/rules/noMapReduceSearchResults.js +33 -0
- package/dist/rules/noMissingModules.js +29 -0
- package/dist/rules/noModuleScopeApiCalls.js +49 -0
- package/dist/rules/noMultipleDefine.js +14 -0
- package/dist/rules/noNativeIterators.js +19 -0
- package/dist/rules/noPositionalOptions.js +26 -0
- package/dist/rules/noPostMessageWildcard.js +15 -0
- package/dist/rules/noPublicFileUpload.js +20 -0
- package/dist/rules/noQueryColumnFieldAndFormula.js +19 -0
- package/dist/rules/noRecordInLoop.js +34 -0
- package/dist/rules/noRequireInsteadOfDefine.js +25 -0
- package/dist/rules/noSecretKeyGuidAndSecret.js +16 -0
- package/dist/rules/noSensitiveBrowserStorage.js +24 -0
- package/dist/rules/noStackInResponse.js +21 -0
- package/dist/rules/noSuiteQLSelectStar.js +18 -0
- package/dist/rules/noSuiteQLSystemNoteJoin.js +19 -0
- package/dist/rules/noSuiteQLTypeInSearch.js +41 -0
- package/dist/rules/noSuiteScript1Api.js +84 -0
- package/dist/rules/noUnknownModuleMember.js +23 -0
- package/dist/rules/noUnknownOptions.js +31 -0
- package/dist/rules/noUnsupportedServerWidgetFieldType.js +40 -0
- package/dist/rules/noWeakHash.js +17 -0
- package/dist/rules/noWildcardCors.js +16 -0
- package/dist/rules/noWrongScriptTypeModules.js +30 -0
- package/dist/rules/requireClientValidateReturn.js +27 -0
- package/dist/rules/requireCustpagePrefix.js +23 -0
- package/dist/rules/requireEachReturn.js +38 -0
- package/dist/rules/requireLogModule.js +25 -0
- package/dist/rules/requireOptions.js +26 -0
- package/dist/rules/requireOrderByForPagedSuiteQL.js +17 -0
- package/dist/rules/requireQueryOptions.js +53 -0
- package/dist/rules/requireServerResponseOptions.js +33 -0
- package/dist/rules/requireServerWidgetOptions.js +37 -0
- package/package.json +3 -1
- package/dist/rules/noExtraModules.js +0 -23
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { optionsObject, propertyValue } from "../lib/options.js";
|
|
3
|
+
const COLUMN_UNSUPPORTED = new Set([
|
|
4
|
+
"CHECKBOX",
|
|
5
|
+
"DATETIME",
|
|
6
|
+
"DATETIMETZ",
|
|
7
|
+
"FILE",
|
|
8
|
+
"HELP",
|
|
9
|
+
"INLINEHTML",
|
|
10
|
+
"LABEL",
|
|
11
|
+
"MULTISELECT",
|
|
12
|
+
"SELECT",
|
|
13
|
+
"RADIO",
|
|
14
|
+
]);
|
|
15
|
+
// Oracle (serverWidget.FieldType): "The DATETIME field type is not supported with
|
|
16
|
+
// addField methods, you must specify DATETIMETZ." The type definitions
|
|
17
|
+
// (N/ui/serverWidget.d.ts) have no DATETIME member in FieldType at all. Oracle also
|
|
18
|
+
// says "The CHECKBOX, DATETIME,
|
|
19
|
+
// DATETIMETZ, FILE, HELP, INLINEHTML, LABEL, MULTISELECT, SELECT, and RADIO field
|
|
20
|
+
// types are not supported with List.addColumn(options)." Sublist.addField has its
|
|
21
|
+
// own list but needs the receiver's type, so it is not checked.
|
|
22
|
+
export const noUnsupportedServerWidgetFieldType = defineRule({
|
|
23
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
24
|
+
run(node) {
|
|
25
|
+
const method = /\.(addField|addColumn)$/.exec(node.callee?.text ?? "")?.[1];
|
|
26
|
+
if (!method)
|
|
27
|
+
return;
|
|
28
|
+
const options = optionsObject(node);
|
|
29
|
+
const type = options && propertyValue(options, "type");
|
|
30
|
+
const member = /(^|\.)FieldType\.(\w+)$/.exec(type?.text ?? "")?.[2];
|
|
31
|
+
if (!type || !member)
|
|
32
|
+
return;
|
|
33
|
+
if ((method === "addField" && member === "DATETIME") ||
|
|
34
|
+
(method === "addColumn" && COLUMN_UNSUPPORTED.has(member))) {
|
|
35
|
+
registerDiagnostic(type, "error", method === "addField"
|
|
36
|
+
? "DATETIME is not supported by addField; use DATETIMETZ."
|
|
37
|
+
: `${member} is not supported by List.addColumn.`);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
// Tier 0: docs/adr/0005-guessing-rules-are-in-no-preset.md.
|
|
3
|
+
// Oracle's OWASP skill: "MD5 and SHA-1 are cryptographically broken" and "Use
|
|
4
|
+
// SHA-256 as the minimum hashing algorithm. MD5 and SHA-1 are broken and must
|
|
5
|
+
// never be used for security-sensitive operations." LOW CONFIDENCE, so no preset
|
|
6
|
+
// includes it: it cannot tell a security use from a vendor integration that
|
|
7
|
+
// requires MD5 or SHA-1. The type definitions (N/crypto.d.ts) list only SHA256 and
|
|
8
|
+
// SHA512 in crypto.HashAlg, so the MD5 and SHA1 member names come from the skill
|
|
9
|
+
// alone; the skill outranks the type definitions, so the rule stays.
|
|
10
|
+
export const noWeakHash = defineRule({
|
|
11
|
+
query: ast("JS_STATIC_MEMBER_EXPRESSION"),
|
|
12
|
+
run(node) {
|
|
13
|
+
if (/(^|\.)HashAlg\.(MD5|SHA1)$/.test(node.text)) {
|
|
14
|
+
registerDiagnostic(node, "warning", "MD5 and SHA-1 are broken; use SHA-256 or stronger for security-sensitive hashing.");
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { headerCall } from "../lib/headers.js";
|
|
3
|
+
import { stringValue } from "../lib/options.js";
|
|
4
|
+
// Oracle's OWASP skill: "Setting `Access-Control-Allow-Origin: *` allows any
|
|
5
|
+
// website to make cross-origin requests to the RESTlet, enabling data theft from
|
|
6
|
+
// authenticated sessions", and "Use explicit CORS origin allowlist (never `*`)".
|
|
7
|
+
export const noWildcardCors = defineRule({
|
|
8
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
9
|
+
run(node) {
|
|
10
|
+
const header = headerCall(node);
|
|
11
|
+
if (header?.name === "access-control-allow-origin" &&
|
|
12
|
+
stringValue(header.value) === "*") {
|
|
13
|
+
registerDiagnostic(header.value ?? node, "warning", "Do not allow every origin with Access-Control-Allow-Origin: *; list the allowed origins.");
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { dependencies } from "../lib/biome-modules.js";
|
|
3
|
+
import { misplacedModule, sideOf, unsupportedModule, } from "../lib/module-script-types.js";
|
|
4
|
+
import { inferredTypes } from "../lib/script-side.js";
|
|
5
|
+
// Loading a client-only module in a server script (or the reverse), or a module
|
|
6
|
+
// a script type does not support, fails at runtime. The type comes from the
|
|
7
|
+
// entry points the module returns, so libraries and uninferable modules are
|
|
8
|
+
// never flagged. The lists follow Oracle's "Supported Script Types" table, except
|
|
9
|
+
// N/ui/message, which the table marks client-only but which server scripts load to
|
|
10
|
+
// build the message that Form.addPageInitMessage shows (see module-script-types.ts).
|
|
11
|
+
export const noWrongScriptTypeModules = defineRule({
|
|
12
|
+
query: ast("JS_CALL_EXPRESSION", "JS_IMPORT"),
|
|
13
|
+
run(node) {
|
|
14
|
+
const deps = dependencies(node);
|
|
15
|
+
if (deps.length === 0)
|
|
16
|
+
return;
|
|
17
|
+
const types = inferredTypes(node);
|
|
18
|
+
if (!types)
|
|
19
|
+
return;
|
|
20
|
+
const side = sideOf(types);
|
|
21
|
+
const single = types.length === 1 ? types[0]?.name : undefined;
|
|
22
|
+
for (const { name, nameNode } of deps) {
|
|
23
|
+
const message = name &&
|
|
24
|
+
((side && misplacedModule(side, name)) ||
|
|
25
|
+
(single && unsupportedModule(single, name)));
|
|
26
|
+
if (message && nameNode)
|
|
27
|
+
registerDiagnostic(nameNode, "error", message);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { descendants, functionName, scriptSide } from "../lib/script-side.js";
|
|
3
|
+
const VALIDATORS = [
|
|
4
|
+
"saveRecord",
|
|
5
|
+
"validateField",
|
|
6
|
+
"validateLine",
|
|
7
|
+
"validateInsert",
|
|
8
|
+
"validateDelete",
|
|
9
|
+
];
|
|
10
|
+
// Oracle: saveRecord and validateField return "true ... false otherwise". Only a
|
|
11
|
+
// body with no `return <value>` at all is flagged; code paths are not analysed.
|
|
12
|
+
export const requireClientValidateReturn = defineRule({
|
|
13
|
+
query: ast("JS_FUNCTION_DECLARATION", "JS_FUNCTION_EXPRESSION", "JS_ARROW_FUNCTION_EXPRESSION", "JS_METHOD_OBJECT_MEMBER"),
|
|
14
|
+
run(node) {
|
|
15
|
+
const name = functionName(node);
|
|
16
|
+
if (!name || !VALIDATORS.includes(name))
|
|
17
|
+
return;
|
|
18
|
+
const body = node.children().find((c) => c.kind === "JS_FUNCTION_BODY");
|
|
19
|
+
if (!body || scriptSide(node) !== "client")
|
|
20
|
+
return;
|
|
21
|
+
for (const d of descendants(body)) {
|
|
22
|
+
if (d.kind === "JS_RETURN_STATEMENT" && d.children().length > 0)
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
registerDiagnostic(node, "warning", `'${name}' must return true or false.`);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { optionsObject, propertyValue } from "../lib/options.js";
|
|
3
|
+
// Receiver `context.form` (or `scriptContext.form`) is the form of the page being
|
|
4
|
+
// loaded, an existing NetSuite page. Forms made with serverWidget.createForm are
|
|
5
|
+
// standalone pages, and Oracle's own samples use unprefixed ids on them.
|
|
6
|
+
const ADDERS = /\.form\.(addField|addButton|addSublist|addTab|addSubtab|addCredentialField|addSecretKeyField)$/;
|
|
7
|
+
// Oracle (N/ui/serverWidget): "When you add a UI object to an existing NetSuite
|
|
8
|
+
// page, to minimize the occurrence of field/object name conflicts, the internal ID
|
|
9
|
+
// that references the object must be prefixed with custpage." Only a string
|
|
10
|
+
// literal `id` is checked.
|
|
11
|
+
export const requireCustpagePrefix = defineRule({
|
|
12
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
13
|
+
run(node) {
|
|
14
|
+
if (!ADDERS.test(node.callee?.text ?? ""))
|
|
15
|
+
return;
|
|
16
|
+
const options = optionsObject(node);
|
|
17
|
+
const id = options && propertyValue(options, "id");
|
|
18
|
+
if (id?.kind === "JS_STRING_LITERAL_EXPRESSION" &&
|
|
19
|
+
!/^['"]custpage/.test(id.text)) {
|
|
20
|
+
registerDiagnostic(id, "warning", "The id of a UI object added to an existing page must start with 'custpage'.");
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { callArguments } from "../lib/options.js";
|
|
3
|
+
import { isFunction } from "../lib/script-side.js";
|
|
4
|
+
const EACH = /\.(run|iterator)\(\)\.each$/;
|
|
5
|
+
/** Whether a function body returns a value, ignoring nested functions. */
|
|
6
|
+
function returnsValue(node) {
|
|
7
|
+
for (const child of node.children()) {
|
|
8
|
+
if (isFunction(child))
|
|
9
|
+
continue;
|
|
10
|
+
if (child.kind === "JS_RETURN_STATEMENT" && child.children()[0])
|
|
11
|
+
return true;
|
|
12
|
+
if (returnsValue(child))
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
// Oracle's upgrade skill: "`.each()` callback must return `true` to continue
|
|
18
|
+
// (stops on `false`)". A callback that never returns a value stops after the first
|
|
19
|
+
// row without an error. Only `.run().each` and `.iterator().each` are checked. A
|
|
20
|
+
// block body must return some value; an expression body that is a call (such as
|
|
21
|
+
// `ids.push(id)`) returns whatever the call does, which is not `true`.
|
|
22
|
+
export const requireEachReturn = defineRule({
|
|
23
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
24
|
+
run(node) {
|
|
25
|
+
if (!EACH.test(node.callee?.text ?? ""))
|
|
26
|
+
return;
|
|
27
|
+
const callback = callArguments(node)[0];
|
|
28
|
+
if (!callback || !isFunction(callback))
|
|
29
|
+
return;
|
|
30
|
+
const body = callback.children().find((c) => c.kind === "JS_FUNCTION_BODY");
|
|
31
|
+
const flagged = body
|
|
32
|
+
? !returnsValue(body)
|
|
33
|
+
: callback.children().at(-1)?.kind === "JS_CALL_EXPRESSION";
|
|
34
|
+
if (flagged) {
|
|
35
|
+
registerDiagnostic(callback, "warning", "The each() callback must return true to continue to the next row.");
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineRule, registerDiagnostic, semantic, } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { inSuiteScriptModule } from "../lib/suitescript.js";
|
|
3
|
+
// Tier 0: docs/adr/0006-log-is-a-global-with-two-opposite-styles.md.
|
|
4
|
+
// Load N/log when you call log.*. Oracle's upgrade skill says `log` is global but
|
|
5
|
+
// "explicitly including it in `define()` makes dependencies clearer and is
|
|
6
|
+
// recommended", and that practice is what this rule enforces. The help center's
|
|
7
|
+
// "NetSuite loads the log Object by default for all script types. You do not need
|
|
8
|
+
// to load it manually" is why noMissingModules leaves `log` alone. The learning
|
|
9
|
+
// skill's "not globally available" loses: the documentation and the higher-ranked
|
|
10
|
+
// upgrade skill both say it is global. This is the opposite style to noLogModule,
|
|
11
|
+
// so enable only one of them.
|
|
12
|
+
export const requireLogModule = defineRule({
|
|
13
|
+
query: semantic("JS_REFERENCE_IDENTIFIER"),
|
|
14
|
+
run(node, { model }) {
|
|
15
|
+
if (node.text !== "log" || model.binding(node))
|
|
16
|
+
return;
|
|
17
|
+
const member = node.parent?.parent;
|
|
18
|
+
if (member?.kind !== "JS_STATIC_MEMBER_EXPRESSION" ||
|
|
19
|
+
member.children()[0]?.text !== "log")
|
|
20
|
+
return;
|
|
21
|
+
if (!inSuiteScriptModule(node))
|
|
22
|
+
return;
|
|
23
|
+
registerDiagnostic(node, "warning", "'log' is used but N/log is not loaded.");
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineRule, registerDiagnostic, semantic, } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { moduleCall } from "../lib/module-calls.js";
|
|
3
|
+
import { definedOptions } from "../lib/required-options.js";
|
|
4
|
+
// The type definitions (Tier 2) say which options each module-level function
|
|
5
|
+
// requires, and a missing one makes the call fail at runtime (Oracle's method
|
|
6
|
+
// pages list it as SSS_MISSING_REQD_ARGUMENT). The receiver is resolved from the
|
|
7
|
+
// define() or import binding, so `record.load` is checked only when `record` is
|
|
8
|
+
// N/record. Only an options object literal without a spread is checked, and a
|
|
9
|
+
// function with several call shapes passes when any shape is satisfied.
|
|
10
|
+
export const requireOptions = defineRule({
|
|
11
|
+
query: semantic("JS_CALL_EXPRESSION"),
|
|
12
|
+
run(node, { model }) {
|
|
13
|
+
const call = moduleCall(node, model);
|
|
14
|
+
if (!call || call.alternatives === 0)
|
|
15
|
+
return;
|
|
16
|
+
const defined = definedOptions(node);
|
|
17
|
+
if (!defined)
|
|
18
|
+
return;
|
|
19
|
+
const missing = call.alternatives
|
|
20
|
+
.map(([required]) => required.filter((name) => !defined.has(name)))
|
|
21
|
+
.sort((a, b) => a.length - b.length)[0];
|
|
22
|
+
for (const name of missing ?? []) {
|
|
23
|
+
registerDiagnostic(node, "error", `${call.callee}() needs the '${name}' option.`);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { staticSuiteql } from "../lib/suiteql.js";
|
|
3
|
+
// Oracle (query.runSuiteQLPaged): "You must specify a sorting order in the query
|
|
4
|
+
// definition when using this method to avoid duplicate or missing results. The
|
|
5
|
+
// query definition must provide a unique and unambiguous sorting order with a
|
|
6
|
+
// specified precedence." Only a query built from literals can be checked.
|
|
7
|
+
export const requireOrderByForPagedSuiteQL = defineRule({
|
|
8
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
9
|
+
run(node) {
|
|
10
|
+
if (!/(^|\.)runSuiteQLPaged(\.promise)?$/.test(node.callee?.text ?? ""))
|
|
11
|
+
return;
|
|
12
|
+
const sql = staticSuiteql(node);
|
|
13
|
+
if (sql && !/\bORDER\s+BY\b/i.test(sql)) {
|
|
14
|
+
registerDiagnostic(node, "warning", "A paged SuiteQL query needs an ORDER BY to avoid duplicate or missing results.");
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { defineRule, registerDiagnostic, semantic, } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { moduleOfReceiver } from "../lib/module-calls.js";
|
|
3
|
+
import { MODULES } from "../lib/modules.js";
|
|
4
|
+
import { definedOptions } from "../lib/required-options.js";
|
|
5
|
+
// Oracle (N/query, Query.createColumn and createCondition): "options.fieldId:
|
|
6
|
+
// required if options.formula is not used" and "options.type: required if
|
|
7
|
+
// options.formula is used"; createCondition's `operator` is required and
|
|
8
|
+
// createSort's `column` is required. The type definitions (N/query.d.ts) outrank
|
|
9
|
+
// the page and give a formula-only condition its own overload without `operator`,
|
|
10
|
+
// so `operator` is required only when there is no `formula`. They type `type?` as
|
|
11
|
+
// optional with the formula, but their own comment says "Required if options.formula
|
|
12
|
+
// is used", which agrees with the page. Only an options object literal without a
|
|
13
|
+
// spread is checked. A receiver bound to another module is skipped:
|
|
14
|
+
// `search.createColumn({ name })` is N/search, which takes different options. When
|
|
15
|
+
// the binding cannot be resolved, a receiver named like another module's
|
|
16
|
+
// conventional identifier (`search`) is skipped too.
|
|
17
|
+
export const requireQueryOptions = defineRule({
|
|
18
|
+
query: semantic("JS_CALL_EXPRESSION"),
|
|
19
|
+
run(node, { model }) {
|
|
20
|
+
const callee = node.callee?.text ?? "";
|
|
21
|
+
const method = /(^|\.)(createColumn|createCondition|createSort)$/.exec(callee)?.[2];
|
|
22
|
+
if (!method)
|
|
23
|
+
return;
|
|
24
|
+
const receiver = node.callee?.children()[0];
|
|
25
|
+
const module = receiver && moduleOfReceiver(receiver, model);
|
|
26
|
+
if (module && module !== "N/query")
|
|
27
|
+
return;
|
|
28
|
+
const conventional = Object.entries(MODULES).find(([, identifier]) => identifier === receiver?.text)?.[0];
|
|
29
|
+
if (!module && conventional && conventional !== "N/query")
|
|
30
|
+
return;
|
|
31
|
+
const defined = definedOptions(node);
|
|
32
|
+
if (!defined)
|
|
33
|
+
return;
|
|
34
|
+
const problems = [];
|
|
35
|
+
if (method === "createSort") {
|
|
36
|
+
if (!defined.has("column"))
|
|
37
|
+
problems.push("needs the 'column' option");
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
if (!defined.has("fieldId") && !defined.has("formula"))
|
|
41
|
+
problems.push("needs a 'fieldId' or a 'formula' option");
|
|
42
|
+
if (defined.has("formula") && !defined.has("type"))
|
|
43
|
+
problems.push("needs the 'type' option when it uses 'formula'");
|
|
44
|
+
if (method === "createCondition" &&
|
|
45
|
+
!defined.has("operator") &&
|
|
46
|
+
!defined.has("formula"))
|
|
47
|
+
problems.push("needs the 'operator' option");
|
|
48
|
+
}
|
|
49
|
+
for (const problem of problems) {
|
|
50
|
+
registerDiagnostic(node, "error", `${method}() ${problem}.`);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { definedOptions } from "../lib/required-options.js";
|
|
3
|
+
const REQUIRED = {
|
|
4
|
+
addHeader: ["name", "value"],
|
|
5
|
+
getHeader: ["name"],
|
|
6
|
+
renderPdf: ["xmlString"],
|
|
7
|
+
sendRedirect: ["type", "identifier"],
|
|
8
|
+
setHeader: ["name", "value"],
|
|
9
|
+
write: ["output"],
|
|
10
|
+
writeFile: ["file"],
|
|
11
|
+
writeLine: ["output"],
|
|
12
|
+
writePage: ["pageObject"],
|
|
13
|
+
};
|
|
14
|
+
// Oracle (ServerResponse method pages): each method lists its required options and
|
|
15
|
+
// throws SSS_MISSING_REQD_ARGUMENT without them. The type definitions
|
|
16
|
+
// (N/http.d.ts) require the same options. Only calls on a receiver named
|
|
17
|
+
// like a response, with an options object literal and no spread, are checked.
|
|
18
|
+
export const requireServerResponseOptions = defineRule({
|
|
19
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
20
|
+
run(node) {
|
|
21
|
+
const match = /response\.(\w+)$/i.exec(node.callee?.text ?? "");
|
|
22
|
+
const method = match?.[1];
|
|
23
|
+
const required = method && REQUIRED[method];
|
|
24
|
+
const defined = required && definedOptions(node);
|
|
25
|
+
if (!required || !defined)
|
|
26
|
+
return;
|
|
27
|
+
for (const name of required) {
|
|
28
|
+
if (!defined.has(name)) {
|
|
29
|
+
registerDiagnostic(node, "error", `${method}() needs the '${name}' option.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
+
import { definedOptions } from "../lib/required-options.js";
|
|
3
|
+
const LABELLED = ["id", "label"];
|
|
4
|
+
const REQUIRED = {
|
|
5
|
+
addButton: ["label"],
|
|
6
|
+
addColumn: [...LABELLED, "type"],
|
|
7
|
+
addCredentialField: [...LABELLED, "restrictToDomains", "restrictToScriptIds"],
|
|
8
|
+
addField: [...LABELLED, "type"],
|
|
9
|
+
addFieldGroup: LABELLED,
|
|
10
|
+
addSecretKeyField: LABELLED,
|
|
11
|
+
addSublist: [...LABELLED, "type"],
|
|
12
|
+
addSubtab: LABELLED,
|
|
13
|
+
addTab: LABELLED,
|
|
14
|
+
};
|
|
15
|
+
// Oracle (N/ui/serverWidget method pages): each add method lists the options it
|
|
16
|
+
// requires and throws SSS_MISSING_REQD_ARGUMENT without them. The type definitions
|
|
17
|
+
// (N/ui/serverWidget.d.ts) outrank the pages and disagree on two methods: they make
|
|
18
|
+
// addButton's `id` optional and addSecretKeyField's `restrictToScriptIds`
|
|
19
|
+
// optional, where the pages require both, so those two follow the type
|
|
20
|
+
// definitions. The receiver cannot
|
|
21
|
+
// be typed, so a method of the same name on another object can misfire. Only an
|
|
22
|
+
// options object literal without a spread is checked.
|
|
23
|
+
export const requireServerWidgetOptions = defineRule({
|
|
24
|
+
query: ast("JS_CALL_EXPRESSION"),
|
|
25
|
+
run(node) {
|
|
26
|
+
const method = /\.(\w+)$/.exec(node.callee?.text ?? "")?.[1];
|
|
27
|
+
const required = method && REQUIRED[method];
|
|
28
|
+
const defined = required && definedOptions(node);
|
|
29
|
+
if (!required || !defined)
|
|
30
|
+
return;
|
|
31
|
+
for (const name of required) {
|
|
32
|
+
if (!defined.has(name)) {
|
|
33
|
+
registerDiagnostic(node, "error", `${method}() needs the '${name}' option.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "biome-plugin-suitescript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Biome lint plugins and a tag checker for NetSuite SuiteScript 2.1, ported from eslint-plugin-suitescript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"biome-plugin",
|
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
"format": "biome check --write",
|
|
42
42
|
"lint": "biome lint",
|
|
43
43
|
"prepack": "npm run build",
|
|
44
|
+
"readme": "node scripts/rules-table.ts",
|
|
44
45
|
"sync-modules": "node scripts/sync-modules.ts",
|
|
46
|
+
"sync-typings": "node scripts/sync-typings.ts",
|
|
45
47
|
"pretest": "npm run build",
|
|
46
48
|
"test": "node --test test/*.test.ts",
|
|
47
49
|
"typecheck": "tsc"
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
|
|
2
|
-
import { parseDefine } from "../lib/define.js";
|
|
3
|
-
export const noExtraModules = defineRule({
|
|
4
|
-
query: ast("JS_CALL_EXPRESSION"),
|
|
5
|
-
run(node) {
|
|
6
|
-
const def = parseDefine(node);
|
|
7
|
-
if (!def)
|
|
8
|
-
return;
|
|
9
|
-
const names = def.elements;
|
|
10
|
-
const params = def.params;
|
|
11
|
-
if (names.length > params.length) {
|
|
12
|
-
for (const extra of names.slice(params.length)) {
|
|
13
|
-
registerDiagnostic(extra, "error", `Excess module name ${extra.text}.`);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
else if (params.length > names.length) {
|
|
17
|
-
for (const extra of params.slice(names.length)) {
|
|
18
|
-
if (extra)
|
|
19
|
-
registerDiagnostic(extra, "error", `Excess module identifier '${extra.text}'.`);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
});
|