biome-plugin-suitescript 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,94 @@
1
+ import { pairs, parseDefine } from "./define.js";
2
+ const unquote = (text) => text.replace(/^['"]|['"]$/g, "");
3
+ function moduleItems(moduleNode) {
4
+ return (moduleNode
5
+ .children()
6
+ .find((c) => c.kind === "JS_MODULE_ITEM_LIST")
7
+ ?.children() ?? []);
8
+ }
9
+ /** Dependencies declared by a node: a define() call or an `import` statement. */
10
+ export function dependencies(node) {
11
+ if (node.kind === "JS_CALL_EXPRESSION") {
12
+ const def = parseDefine(node);
13
+ return def ? pairs(def) : [];
14
+ }
15
+ if (node.kind !== "JS_IMPORT")
16
+ return [];
17
+ const clause = node.children()[0];
18
+ const source = clause?.children().find((c) => c.kind === "JS_MODULE_SOURCE");
19
+ if (!clause || !source)
20
+ return [];
21
+ const isBinding = clause.kind === "JS_IMPORT_DEFAULT_CLAUSE" ||
22
+ clause.kind === "JS_IMPORT_NAMESPACE_CLAUSE";
23
+ const param = isBinding
24
+ ? clause
25
+ .children()[0]
26
+ ?.children()
27
+ .find((c) => c.kind === "JS_IDENTIFIER_BINDING")
28
+ : undefined;
29
+ return [{ name: source.text.slice(1, -1), nameNode: source, param }];
30
+ }
31
+ /** Whether an ES module imports at least one `N/` module (so it is SuiteScript). */
32
+ export function importsNetSuite(moduleNode) {
33
+ return moduleItems(moduleNode).some((item) => dependencies(item).some((d) => d.name === "N" || d.name?.startsWith("N/")));
34
+ }
35
+ function memberName(member) {
36
+ const name = member.children()[0]?.text;
37
+ return name === undefined ? undefined : unquote(name);
38
+ }
39
+ /** Property names of an object literal; `opaque` when a spread hides some. */
40
+ export function objectNames(object) {
41
+ const members = object.children()[0]?.children() ?? [];
42
+ const names = [];
43
+ let opaque = false;
44
+ for (const m of members) {
45
+ if (m.kind === "JS_SPREAD") {
46
+ opaque = true;
47
+ }
48
+ else {
49
+ const name = memberName(m);
50
+ if (name)
51
+ names.push(name);
52
+ }
53
+ }
54
+ return { names, opaque };
55
+ }
56
+ /** Names exported by an ES module: `export const x`, `export function x`, `export { a as b }`. */
57
+ export function esExportNames(moduleNode) {
58
+ const names = [];
59
+ for (const item of moduleItems(moduleNode)) {
60
+ if (item.kind !== "JS_EXPORT")
61
+ continue;
62
+ const clause = item.children().find((c) => c.kind !== "JS_DECORATOR_LIST");
63
+ if (!clause)
64
+ continue;
65
+ if (clause.kind === "JS_FUNCTION_DECLARATION") {
66
+ const id = clause
67
+ .children()
68
+ .find((c) => c.kind === "JS_IDENTIFIER_BINDING");
69
+ if (id)
70
+ names.push(id.text);
71
+ }
72
+ else if (clause.kind === "JS_VARIABLE_DECLARATION_CLAUSE") {
73
+ const declarators = clause.children()[0]?.children()[0]?.children() ?? [];
74
+ for (const d of declarators) {
75
+ const id = d.children().find((c) => c.kind === "JS_IDENTIFIER_BINDING");
76
+ if (id)
77
+ names.push(id.text);
78
+ }
79
+ }
80
+ else if (clause.kind === "JS_EXPORT_NAMED_CLAUSE") {
81
+ for (const specifier of clause.children()[0]?.children() ?? []) {
82
+ const exported = specifier
83
+ .children()
84
+ .find((c) => c.kind === "JS_LITERAL_EXPORT_NAME");
85
+ const name = exported
86
+ ? unquote(exported.text)
87
+ : specifier.children()[0]?.text;
88
+ if (name)
89
+ names.push(name);
90
+ }
91
+ }
92
+ }
93
+ return names;
94
+ }
@@ -0,0 +1,72 @@
1
+ const FUNCTION_KINDS = [
2
+ "JS_FUNCTION_EXPRESSION",
3
+ "JS_ARROW_FUNCTION_EXPRESSION",
4
+ ];
5
+ /** Strips the quotes from a string literal expression node. */
6
+ export function stringValue(node) {
7
+ if (node?.kind !== "JS_STRING_LITERAL_EXPRESSION")
8
+ return undefined;
9
+ return node.text.slice(1, -1);
10
+ }
11
+ /**
12
+ * Parses an AMD `define([...], function (...) {})` call.
13
+ *
14
+ * Returns undefined unless `call` is `define(...)` with a dependency array
15
+ * followed by a function callback (the optional leading module id is allowed).
16
+ */
17
+ export function parseDefine(call) {
18
+ if (call.callee?.text !== "define")
19
+ return undefined;
20
+ const args = call.arguments?.children()[0]?.children() ?? [];
21
+ if (args.length < 2 || args.length > 3)
22
+ return undefined;
23
+ const deps = args[args.length - 2];
24
+ const callback = args[args.length - 1];
25
+ if (!deps || !callback)
26
+ return undefined;
27
+ if (deps.kind !== "JS_ARRAY_EXPRESSION")
28
+ return undefined;
29
+ if (!FUNCTION_KINDS.includes(callback.kind))
30
+ return undefined;
31
+ const elements = deps.children()[0]?.children() ?? [];
32
+ const callbackChildren = callback.children();
33
+ let params = [];
34
+ const parameters = callbackChildren.find((c) => c.kind === "JS_PARAMETERS");
35
+ if (parameters) {
36
+ params = (parameters.children()[0]?.children() ?? []).map((p) => p.kind === "JS_FORMAL_PARAMETER"
37
+ ? p.children().find((c) => c.kind === "JS_IDENTIFIER_BINDING")
38
+ : undefined);
39
+ }
40
+ else if (callback.kind === "JS_ARROW_FUNCTION_EXPRESSION") {
41
+ // `x => ...` has a bare identifier binding instead of a parameter list.
42
+ const single = callbackChildren.find((c) => c.kind === "JS_IDENTIFIER_BINDING");
43
+ if (single)
44
+ params = [single];
45
+ }
46
+ const body = callbackChildren.find((c) => c.kind === "JS_FUNCTION_BODY" ||
47
+ (c.kind.endsWith("_EXPRESSION") && c !== parameters));
48
+ return {
49
+ body,
50
+ call,
51
+ callback,
52
+ deps,
53
+ elements,
54
+ id: args.length === 3 ? args[0] : undefined,
55
+ params,
56
+ };
57
+ }
58
+ /** Pairs each dependency string with its callback parameter, by position. */
59
+ export function pairs(def) {
60
+ const length = Math.max(def.elements.length, def.params.length);
61
+ const out = [];
62
+ for (let i = 0; i < length; i++) {
63
+ const element = def.elements[i];
64
+ const isString = element?.kind === "JS_STRING_LITERAL_EXPRESSION";
65
+ out.push({
66
+ name: stringValue(element),
67
+ nameNode: isString ? element : undefined,
68
+ param: def.params[i],
69
+ });
70
+ }
71
+ return out;
72
+ }
@@ -0,0 +1,67 @@
1
+ import { objectNames } from "./biome-modules.js";
2
+ function unwrap(node) {
3
+ let current = node;
4
+ while (current?.kind === "JS_PARENTHESIZED_EXPRESSION")
5
+ current = current.children()[0];
6
+ return current;
7
+ }
8
+ /**
9
+ * Names returned by a define() callback, or undefined when they cannot be
10
+ * determined statically (spreads, calls, conditionals).
11
+ */
12
+ export function amdReturnedNames(def) {
13
+ if (!def.body)
14
+ return undefined;
15
+ let statements = [];
16
+ let returned;
17
+ if (def.body.kind === "JS_FUNCTION_BODY") {
18
+ statements =
19
+ def.body
20
+ .children()
21
+ .find((c) => c.kind === "JS_STATEMENT_LIST")
22
+ ?.children() ?? [];
23
+ returned = unwrap(statements.find((s) => s.kind === "JS_RETURN_STATEMENT")?.children()[0]);
24
+ }
25
+ else {
26
+ returned = unwrap(def.body);
27
+ }
28
+ if (returned?.kind === "JS_OBJECT_EXPRESSION") {
29
+ const { names, opaque } = objectNames(returned);
30
+ return opaque ? undefined : names;
31
+ }
32
+ if (returned?.kind !== "JS_IDENTIFIER_EXPRESSION")
33
+ return returned ? undefined : [];
34
+ // `const o = {...}; o.pageInit = ...; return o;`
35
+ const variable = returned.text;
36
+ const names = [];
37
+ for (const s of statements) {
38
+ if (s.kind === "JS_VARIABLE_STATEMENT") {
39
+ const declarators = s.children()[0]?.children()[0]?.children() ?? [];
40
+ for (const d of declarators) {
41
+ const id = d.children().find((c) => c.kind === "JS_IDENTIFIER_BINDING");
42
+ const initializer = d
43
+ .children()
44
+ .find((c) => c.kind === "JS_INITIALIZER_CLAUSE");
45
+ const init = unwrap(initializer?.children()[0]);
46
+ if (id?.text === variable && init?.kind === "JS_OBJECT_EXPRESSION") {
47
+ const { names: found, opaque } = objectNames(init);
48
+ if (opaque)
49
+ return undefined;
50
+ names.push(...found);
51
+ }
52
+ }
53
+ }
54
+ else if (s.kind === "JS_EXPRESSION_STATEMENT") {
55
+ const assign = s.children()[0];
56
+ const target = assign?.kind === "JS_ASSIGNMENT_EXPRESSION"
57
+ ? assign.children()[0]
58
+ : undefined;
59
+ if (target?.kind === "JS_STATIC_MEMBER_ASSIGNMENT") {
60
+ const [object, property] = target.children();
61
+ if (object?.text === variable && property)
62
+ names.push(property.text);
63
+ }
64
+ }
65
+ }
66
+ return names;
67
+ }
@@ -0,0 +1,92 @@
1
+ // Pure logic shared by the Biome rules and the CLI. No imports from Biome or Node.
2
+ import { SCRIPT_TYPES } from "./script-types.js";
3
+ export const ALL_ENTRY_POINTS = new Set(SCRIPT_TYPES.flatMap((t) => t.entryPoints));
4
+ // Names that commonly appear in ordinary library modules. A module that only
5
+ // returns these is not treated as a script, so libraries are never flagged.
6
+ export const GENERIC_ENTRY_POINTS = new Set([
7
+ "get",
8
+ "post",
9
+ "put",
10
+ "delete",
11
+ "map",
12
+ "reduce",
13
+ "run",
14
+ "render",
15
+ ]);
16
+ export function typeByName(name) {
17
+ return SCRIPT_TYPES.find((t) => t.name.toLowerCase() === name.toLowerCase());
18
+ }
19
+ /** Extra requirements on top of "returns at least one entry point of the type". */
20
+ function missingRequired(type, names) {
21
+ if (type.name === "MapReduceScript") {
22
+ const missing = [];
23
+ if (!names.includes("getInputData"))
24
+ missing.push("getInputData");
25
+ if (!names.includes("map") && !names.includes("reduce"))
26
+ missing.push("map or reduce");
27
+ return missing;
28
+ }
29
+ return [];
30
+ }
31
+ /**
32
+ * Infers the script type from the entry points a module returns.
33
+ *
34
+ * Returns undefined for libraries (nothing distinctive returned). Otherwise
35
+ * `types` are the script types that could own every known entry point; empty
36
+ * `types` means the entry points are mixed.
37
+ */
38
+ export function inferScriptType(returnedNames) {
39
+ const known = [...new Set(returnedNames)].filter((n) => ALL_ENTRY_POINTS.has(n));
40
+ if (!known.some((n) => !GENERIC_ENTRY_POINTS.has(n)))
41
+ return undefined;
42
+ const types = SCRIPT_TYPES.filter((t) => known.every((n) => t.entryPoints.includes(n)));
43
+ return { known, types };
44
+ }
45
+ /** Problems with an inferred script type, as messages. Empty when valid. */
46
+ export function inferredProblems(returnedNames) {
47
+ const inferred = inferScriptType(returnedNames);
48
+ if (!inferred)
49
+ return [];
50
+ if (inferred.types.length === 0) {
51
+ return [
52
+ `Entry points ${inferred.known.join(", ")} do not belong to a single script type.`,
53
+ ];
54
+ }
55
+ const names = [...new Set(returnedNames)];
56
+ const failures = inferred.types.map((type) => ({
57
+ missing: missingRequired(type, names),
58
+ type,
59
+ }));
60
+ if (failures.some((f) => f.missing.length === 0))
61
+ return [];
62
+ const first = failures[0];
63
+ return first
64
+ ? [
65
+ `${first.type.name} requires entry point ${first.missing.join(" and ")}.`,
66
+ ]
67
+ : [];
68
+ }
69
+ /** Problems with a script type declared by `@NScriptType`, given what is returned. */
70
+ export function declaredProblems(typeName, returnedNames) {
71
+ const type = typeByName(typeName);
72
+ if (!type || type.entryPoints.length === 0)
73
+ return [];
74
+ const names = [...new Set(returnedNames)];
75
+ const foreign = names.filter((n) => ALL_ENTRY_POINTS.has(n) &&
76
+ !type.entryPoints.includes(n) &&
77
+ !GENERIC_ENTRY_POINTS.has(n));
78
+ const problems = [];
79
+ if (foreign.length) {
80
+ problems.push(`@NScriptType ${type.name} does not support returned entry point${foreign.length > 1 ? "s" : ""} ${foreign.join(", ")}.`);
81
+ }
82
+ if (!names.some((n) => type.entryPoints.includes(n))) {
83
+ if (!foreign.length)
84
+ problems.push(`Return at least one valid ${type.name} entry point.`);
85
+ }
86
+ else {
87
+ const missing = missingRequired(type, names);
88
+ if (missing.length)
89
+ problems.push(`${type.name} requires entry point ${missing.join(" and ")}.`);
90
+ }
91
+ return problems;
92
+ }
@@ -0,0 +1,43 @@
1
+ import { dependencies } from "./biome-modules.js";
2
+ const LOG_MEMBERS = ["debug", "audit", "error", "emergency"];
3
+ /** Name the N/log module is bound to (define() or import), else `log`. */
4
+ function logIdentifier(call) {
5
+ for (const ancestor of call.ancestors()) {
6
+ const scopes = ancestor.kind === "JS_MODULE"
7
+ ? (ancestor
8
+ .children()
9
+ .find((c) => c.kind === "JS_MODULE_ITEM_LIST")
10
+ ?.children() ?? [])
11
+ : [ancestor];
12
+ for (const scope of scopes) {
13
+ const found = dependencies(scope).find((d) => d.name === "N/log" && d.param);
14
+ if (found?.param)
15
+ return found.param.text;
16
+ }
17
+ }
18
+ return "log";
19
+ }
20
+ /** Matches `log.<debug|audit|error|emergency>(...)` calls that pass arguments. */
21
+ export function logCall(call) {
22
+ const callee = call.callee;
23
+ if (callee?.kind !== "JS_STATIC_MEMBER_EXPRESSION")
24
+ return undefined;
25
+ const [object, member] = callee.children();
26
+ if (!object || !member || !LOG_MEMBERS.includes(member.text))
27
+ return undefined;
28
+ if (object.text !== logIdentifier(call))
29
+ return undefined;
30
+ const args = call.arguments?.children()[0]?.children() ?? [];
31
+ const firstArg = args[0];
32
+ if (!firstArg)
33
+ return undefined;
34
+ return { args, firstArg, prop: member.text };
35
+ }
36
+ /** Whether an object literal argument defines `key`. */
37
+ export function hasProperty(objectNode, key) {
38
+ const members = objectNode.children()[0]?.children() ?? [];
39
+ return members.some((m) => {
40
+ const name = m.children()[0]?.text;
41
+ return name === key || name === `"${key}"` || name === `'${key}'`;
42
+ });
43
+ }
@@ -0,0 +1,61 @@
1
+ // Valid SuiteScript 2.x modules and their conventional identifiers.
2
+ // Generated by scripts/sync-modules.ts from @hitc/netsuite-types.
3
+ export const MODULES = {
4
+ "N/action": "action",
5
+ "N/auth": "auth",
6
+ "N/cache": "cache",
7
+ "N/certificateControl": "certificateControl",
8
+ "N/commerce/recordView": "recordView",
9
+ "N/compress": "compress",
10
+ "N/config": "config",
11
+ "N/crypto": "crypto",
12
+ "N/crypto/certificate": "certificate",
13
+ "N/crypto/random": "random",
14
+ "N/currency": "currency",
15
+ "N/currentRecord": "currentRecord",
16
+ "N/dataset": "dataset",
17
+ "N/datasetLink": "datasetLink",
18
+ "N/documentCapture": "documentCapture",
19
+ "N/email": "email",
20
+ "N/encode": "encode",
21
+ "N/error": "error",
22
+ "N/file": "file",
23
+ "N/format": "format",
24
+ "N/format/i18n": "i18n",
25
+ "N/http": "http",
26
+ "N/https": "https",
27
+ "N/https/clientCertificate": "clientCertificate",
28
+ "N/keyControl": "keyControl",
29
+ "N/llm": "llm",
30
+ "N/log": "log",
31
+ "N/machineTranslation": "machineTranslation",
32
+ "N/manufacturing/productionCharges": "productionCharges",
33
+ "N/pgp": "pgp",
34
+ "N/piremoval": "piremoval",
35
+ "N/plugin": "plugin",
36
+ "N/portlet": "portlet",
37
+ "N/query": "query",
38
+ "N/record": "record",
39
+ "N/recordContext": "recordContext",
40
+ "N/redirect": "redirect",
41
+ "N/render": "render",
42
+ "N/runtime": "runtime",
43
+ "N/scriptTypes/restlet": "restlet",
44
+ "N/search": "search",
45
+ "N/sftp": "sftp",
46
+ "N/sso": "sso",
47
+ "N/suiteAppInfo": "suiteAppInfo",
48
+ "N/task": "task",
49
+ "N/task/accounting/recognition": "recognition",
50
+ "N/transaction": "transaction",
51
+ "N/translation": "translation",
52
+ "N/types": "types",
53
+ "N/ui/dialog": "dialog",
54
+ "N/ui/message": "message",
55
+ "N/ui/serverWidget": "serverWidget",
56
+ "N/url": "url",
57
+ "N/util": "util",
58
+ "N/workbook": "workbook",
59
+ "N/workflow": "workflow",
60
+ "N/xml": "xml",
61
+ };
@@ -0,0 +1,105 @@
1
+ export const SCRIPT_TYPES = [
2
+ {
3
+ entryPoints: [
4
+ "getRequiredConfigurationFields",
5
+ "downloadPreviousDayBankStatementFile",
6
+ ],
7
+ name: "BankConnectivityPlugin",
8
+ },
9
+ {
10
+ entryPoints: [
11
+ "afterInstall",
12
+ "afterUpdate",
13
+ "beforeInstall",
14
+ "beforeUninstall",
15
+ "beforeUpdate",
16
+ ],
17
+ name: "BundleInstallationScript",
18
+ },
19
+ {
20
+ entryPoints: [
21
+ "fieldChanged",
22
+ "lineInit",
23
+ "pageInit",
24
+ "postSourcing",
25
+ "saveRecord",
26
+ "sublistChanged",
27
+ "validateDelete",
28
+ "validateField",
29
+ "validateInsert",
30
+ "validateLine",
31
+ "localizationContextEnter",
32
+ "localizationContextExit",
33
+ ],
34
+ name: "ClientScript",
35
+ },
36
+ {
37
+ entryPoints: ["customizeGlImpact"],
38
+ name: "CustomGLPlugin",
39
+ },
40
+ {
41
+ entryPoints: ["createDataset"],
42
+ name: "DatasetBuilderPlugin",
43
+ },
44
+ {
45
+ entryPoints: [
46
+ "getConfigurationIFrameUrl",
47
+ "getAccounts",
48
+ "getTransactionData",
49
+ ],
50
+ name: "FiConnectivityPlugin",
51
+ },
52
+ {
53
+ entryPoints: [
54
+ "parseData",
55
+ "getStandardTransactionCodes",
56
+ "getExpenseCodes",
57
+ "getConfigurationPageUrl",
58
+ ],
59
+ name: "FiParserPlugin",
60
+ },
61
+ {
62
+ entryPoints: ["getInputData", "map", "reduce", "summarize"],
63
+ name: "MapReduceScript",
64
+ },
65
+ {
66
+ entryPoints: ["each"],
67
+ name: "MassUpdateScript",
68
+ },
69
+ {
70
+ entryPoints: [],
71
+ name: "PluginTypeImpl",
72
+ },
73
+ {
74
+ entryPoints: ["render"],
75
+ name: "Portlet",
76
+ },
77
+ {
78
+ entryPoints: ["delete", "get", "post", "put"],
79
+ name: "Restlet",
80
+ },
81
+ {
82
+ entryPoints: ["execute"],
83
+ name: "ScheduledScript",
84
+ },
85
+ {
86
+ entryPoints: ["run"],
87
+ name: "SDFInstallationScript",
88
+ },
89
+ {
90
+ entryPoints: ["onRequest"],
91
+ name: "Suitelet",
92
+ },
93
+ {
94
+ entryPoints: ["afterSubmit", "beforeLoad", "beforeSubmit"],
95
+ name: "UserEventScript",
96
+ },
97
+ {
98
+ entryPoints: ["createWorkbook"],
99
+ name: "WorkbookBbuilderPlugin",
100
+ },
101
+ {
102
+ entryPoints: ["onAction"],
103
+ name: "WorkflowActionScript",
104
+ },
105
+ ];
@@ -0,0 +1,33 @@
1
+ import { ast, defineRule, registerDiagnostic, } from "@biomejs/runtime/plugin";
2
+ import { esExportNames, importsNetSuite } from "../lib/biome-modules.js";
3
+ import { parseDefine } from "../lib/define.js";
4
+ import { amdReturnedNames } from "../lib/entry-points.js";
5
+ import { inferredProblems } from "../lib/inference.js";
6
+ // Infers the script type from the entry points a module returns and checks
7
+ // they belong to one type and that its required entry points are present.
8
+ // Modules that return no distinctive entry point (libraries) are ignored.
9
+ export const entryPoints = defineRule({
10
+ query: ast("JS_CALL_EXPRESSION", "JS_MODULE"),
11
+ run(node) {
12
+ let names;
13
+ let reportOn = node;
14
+ if (node.kind === "JS_CALL_EXPRESSION") {
15
+ const def = parseDefine(node);
16
+ if (!def)
17
+ return;
18
+ names = amdReturnedNames(def);
19
+ }
20
+ else {
21
+ if (!importsNetSuite(node))
22
+ return;
23
+ names = esExportNames(node);
24
+ reportOn =
25
+ node.children().find((c) => c.kind === "JS_MODULE_ITEM_LIST") ?? node;
26
+ }
27
+ if (!names)
28
+ return;
29
+ for (const message of inferredProblems(names)) {
30
+ registerDiagnostic(reportOn, "error", message);
31
+ }
32
+ },
33
+ });
@@ -0,0 +1,16 @@
1
+ import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
2
+ import { hasProperty, logCall } from "../lib/log.js";
3
+ export const logRequireDetails = defineRule({
4
+ query: ast("JS_CALL_EXPRESSION"),
5
+ run(node) {
6
+ const info = logCall(node);
7
+ if (!info)
8
+ return;
9
+ const { firstArg, args, prop } = info;
10
+ const isObject = firstArg.kind === "JS_OBJECT_EXPRESSION";
11
+ if ((isObject && !hasProperty(firstArg, "details")) ||
12
+ (!isObject && args.length === 1)) {
13
+ registerDiagnostic(node, "warning", `Log details are required with 'log.${prop}'.`);
14
+ }
15
+ },
16
+ });
@@ -0,0 +1,15 @@
1
+ import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
2
+ import { hasProperty, logCall } from "../lib/log.js";
3
+ export const logRequireTitle = defineRule({
4
+ query: ast("JS_CALL_EXPRESSION"),
5
+ run(node) {
6
+ const info = logCall(node);
7
+ if (!info)
8
+ return;
9
+ const { firstArg, prop } = info;
10
+ if (firstArg.kind === "JS_OBJECT_EXPRESSION" &&
11
+ !hasProperty(firstArg, "title")) {
12
+ registerDiagnostic(node, "warning", `A log title is required with 'log.${prop}'.`);
13
+ }
14
+ },
15
+ });
@@ -0,0 +1,16 @@
1
+ import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
2
+ import { dependencies } from "../lib/biome-modules.js";
3
+ import { MODULES } from "../lib/modules.js";
4
+ // Conventional identifiers: N/record -> record, N/ui/serverWidget -> serverWidget.
5
+ // Use `biome-plugin-suitescript generate module-vars` for a custom mapping.
6
+ export const moduleVars = defineRule({
7
+ query: ast("JS_CALL_EXPRESSION", "JS_IMPORT"),
8
+ run(node) {
9
+ for (const { name, param } of dependencies(node)) {
10
+ const expected = name && Object.hasOwn(MODULES, name) ? MODULES[name] : undefined;
11
+ if (expected && param && param.text !== expected) {
12
+ registerDiagnostic(param, "warning", `Identifier for ${name} should be '${expected}'.`);
13
+ }
14
+ }
15
+ },
16
+ });
@@ -0,0 +1,9 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ // Anonymous AMD modules only: NetSuite/bundlers assign the module ID.
5
+ `define($id, $deps, $cb)` as $call where {
6
+ $id <: JsStringLiteralExpression(),
7
+ register_diagnostic(span=$id, message="No AMD module name should be provided.", severity="error", fix_kind="safe"),
8
+ $call => `define($deps, $cb)`
9
+ }
@@ -0,0 +1,23 @@
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
+ });
@@ -0,0 +1,14 @@
1
+ import { ast, defineRule, registerDiagnostic } from "@biomejs/runtime/plugin";
2
+ import { dependencies } from "../lib/biome-modules.js";
3
+ import { MODULES } from "../lib/modules.js";
4
+ // Unknown `N/...` modules are almost always typos; custom modules are fine.
5
+ export const noInvalidModules = defineRule({
6
+ query: ast("JS_CALL_EXPRESSION", "JS_IMPORT"),
7
+ run(node) {
8
+ for (const { nameNode, name } of dependencies(node)) {
9
+ if (nameNode && name?.startsWith("N/") && !Object.hasOwn(MODULES, name)) {
10
+ registerDiagnostic(nameNode, "error", `Invalid NetSuite module '${name}'.`);
11
+ }
12
+ }
13
+ },
14
+ });