UNPKG

eslint-plugin-roblox-ts-x

Version:

A collection of ESLint rules specifically targeted for roblox-ts.

1,549 lines (1,523 loc) 54.9 kB
import { getConstrainedTypeAtLocation, isBuiltinSymbolLike, isTypeFlagSet } from "@typescript-eslint/type-utils"; import { AST_NODE_TYPES, AST_TOKEN_TYPES, TSESTree } from "@typescript-eslint/utils"; import { RuleCreator, getParserServices } from "@typescript-eslint/utils/eslint-utils"; import ts9, { TypeFlags, isArrayLiteralExpression, isObjectLiteralExpression, isPropertyAccessExpression } from "typescript"; //#region package.json var name = "eslint-plugin-roblox-ts-x"; var version = "2.1.0"; //#endregion //#region src/util.ts const createEslintRule = RuleCreator((name$1) => { return `https://github.com/christopher-buss/eslint-plugin-roblox-ts-x/tree/main/src/rules/${name$1}/documentation.md`; }); function assert(condition, message) { if (!condition) throw new Error(message); } //#endregion //#region src/utils/types.ts const robloxTypes = new Set([ "CFrame", "UDim", "UDim2", "Vector2", "Vector2int16", "Vector3", "Vector3int16" ]); function getRobloxDataTypeName(type) { const symbol = type.getSymbol(); if (!symbol) return void 0; const name$1 = symbol.getName(); return robloxTypes.has(name$1) ? name$1 : void 0; } function getRobloxDataTypeNameRecursive(type) { let foundType; isTypeRecursive(type, (innerType) => { const directResult = getRobloxDataTypeName(innerType); if (directResult === void 0) return false; foundType = directResult; return true; }); return foundType; } function isArrayType(checker, type) { return isTypeRecursive(type, (inner) => checker.isArrayLikeType(inner) && !isUnconstrainedType(inner)); } function isDefinedType(type) { return type.flags === TypeFlags.Object && type.getProperties().length === 0 && type.getCallSignatures().length === 0 && type.getConstructSignatures().length === 0 && type.getNumberIndexType() === void 0 && type.getStringIndexType() === void 0; } function isEmptyStringType(type) { if (type.isStringLiteral()) return type.value === ""; return isStringType(type); } function isFunction(type) { return isTypeFlagSet(type, TypeFlags.Object) && type.getCallSignatures().length > 0; } function isIterableFunctionType(program, type) { return isBuiltinSymbolLike(program, type, ["IterableFunction"]); } function isMapType(program, type) { return isBuiltinSymbolLike(program, type, [ "Map", "ReadonlyMap", "WeakMap" ]); } function isNumberLiteralType(type, value) { if (type.isNumberLiteral()) return type.value === value; return isNumberType(type); } function isNumberType(type) { return isTypeFlagSet(type, TypeFlags.NumberLike); } function isPossiblyType(type, callback) { const constrainedType = type.getConstraint() ?? type; return isTypeRecursive(constrainedType, (innerType) => { return isUnconstrainedType(innerType) || isDefinedType(innerType) || callback(innerType); }); } function isSetType(program, type) { return isBuiltinSymbolLike(program, type, [ "Set", "ReadonlySet", "WeakSet" ]); } function isStringType(type) { return isTypeFlagSet(type, TypeFlags.StringLike); } function isUnconstrainedType(type) { return isTypeFlagSet(type, TypeFlags.Any | TypeFlags.Unknown | TypeFlags.TypeVariable); } function isTypeRecursive(type, predicate) { if (type.isUnionOrIntersection()) return type.types.some((inner) => isTypeRecursive(inner, predicate)); return predicate(type); } //#endregion //#region src/rules/lua-truthiness/rule.ts const RULE_NAME$22 = "lua-truthiness"; const FALSY_STRING_NUMBER_CHECK = "falsy-string-number-check"; const messages$22 = { [FALSY_STRING_NUMBER_CHECK]: "0, NaN, and \"\" are falsy in TS. If intentional, disable this rule by placing `\"roblox-ts-x/lua-truthiness\": \"off\"` in your eslint.config file in the \"rules\" object." }; function checkTruthy(context, parserServices, node) { const type = getConstrainedTypeAtLocation(parserServices, node); const isAssignableToZero = isPossiblyType(type, (inner) => isNumberLiteralType(inner, 0)); const isAssignableToEmptyString = isPossiblyType(type, (inner) => isEmptyStringType(inner)); if (isAssignableToZero || isAssignableToEmptyString) context.report({ fix: void 0, messageId: FALSY_STRING_NUMBER_CHECK, node }); } function create$22(context) { const parserServices = getParserServices(context); function containsBoolean() { return ({ test }) => { if (test && test.type !== TSESTree.AST_NODE_TYPES.LogicalExpression) checkTruthy(context, parserServices, test); }; } return { "ConditionalExpression": containsBoolean(), "DoWhileStatement": containsBoolean(), "ForStatement": containsBoolean(), "IfStatement": containsBoolean(), "LogicalExpression": ({ left, operator, parent, right }) => { if (operator !== "??") { checkTruthy(context, parserServices, left); return; } if (parent.type === TSESTree.AST_NODE_TYPES.IfStatement) checkTruthy(context, parserServices, right); }, "UnaryExpression[operator=\"!\"]": ({ argument }) => { checkTruthy(context, parserServices, argument); }, "WhileStatement": containsBoolean() }; } const luaTruthiness = createEslintRule({ create: create$22, defaultOptions: [], meta: { docs: { description: "Enforces the use of lua truthiness", recommended: true, requiresTypeChecking: true }, messages: messages$22, schema: [], type: "problem" }, name: RULE_NAME$22 }); //#endregion //#region node_modules/.pnpm/ts-api-utils@2.1.0_typescript@5.8.3/node_modules/ts-api-utils/lib/index.js function isFlagSet(allFlags, flag) { return (allFlags & flag) !== 0; } function isFlagSetOnObject(obj, flag) { return isFlagSet(obj.flags, flag); } function isObjectFlagSet(objectType, flag) { return isFlagSet(objectType.objectFlags, flag); } var isTypeFlagSet$1 = isFlagSetOnObject; var [tsMajor, tsMinor] = ts9.versionMajorMinor.split(".").map((raw) => Number.parseInt(raw, 10)); function isArrayBindingOrAssignmentPattern(node) { return ts9.isArrayBindingPattern(node) || ts9.isArrayLiteralExpression(node); } var IntrinsicTypeFlags = ts9.TypeFlags.Intrinsic ?? ts9.TypeFlags.Any | ts9.TypeFlags.Unknown | ts9.TypeFlags.String | ts9.TypeFlags.Number | ts9.TypeFlags.BigInt | ts9.TypeFlags.Boolean | ts9.TypeFlags.BooleanLiteral | ts9.TypeFlags.ESSymbol | ts9.TypeFlags.Void | ts9.TypeFlags.Undefined | ts9.TypeFlags.Null | ts9.TypeFlags.Never | ts9.TypeFlags.NonPrimitive; function isObjectType(type) { return isTypeFlagSet$1(type, ts9.TypeFlags.Object); } function isTypeReference(type) { return isObjectType(type) && isObjectFlagSet(type, ts9.ObjectFlags.Reference); } //#endregion //#region src/rules/misleading-lua-tuple-checks/rule.ts const RULE_NAME$21 = "misleading-lua-tuple-checks"; const BANNED_LUA_TUPLE_CHECK = "misleading-lua-tuple-check"; const LUA_TUPLE_DECLARATION = "lua-tuple-declaration"; const messages$21 = { [BANNED_LUA_TUPLE_CHECK]: "Unexpected LuaTuple in conditional expression. Add [0].", [LUA_TUPLE_DECLARATION]: "Unexpected LuaTuple in declaration, use array destructuring." }; function checkLuaTupleUsage(context, parserServices, node) { if (isLuaTuple(parserServices, node)) context.report({ fix: (fixer) => fixer.insertTextAfter(node, "[0]"), messageId: BANNED_LUA_TUPLE_CHECK, node }); } function create$21(context) { const parserServices = getParserServices(context); function containsBoolean({ test }) { if (test && test.type !== AST_NODE_TYPES.LogicalExpression) checkLuaTupleUsage(context, parserServices, test); } return { "AssignmentExpression[operator=\"=\"][left.type=\"Identifier\"]": (node) => { validateAssignmentExpression(context, parserServices, node); }, "ConditionalExpression, DoWhileStatement, IfStatement, ForStatement, WhileStatement": containsBoolean, "ForOfStatement": (node) => { validateForOfStatement(context, parserServices, node); }, "LogicalExpression": ({ left, right }) => { checkLuaTupleUsage(context, parserServices, left); checkLuaTupleUsage(context, parserServices, right); }, "UnaryExpression[operator=\"!\"]": ({ argument }) => { checkLuaTupleUsage(context, parserServices, argument); }, "VariableDeclarator[id.type=\"Identifier\"]": (node) => { validateVariableDeclarator(context, parserServices, node); } }; } function ensureArrayDestructuring(context, parserServices, leftNode) { const esNode = parserServices.esTreeNodeToTSNodeMap.get(leftNode); if (isArrayBindingOrAssignmentPattern(esNode)) return; const fixer = fixIntoArrayDestructuring(context, leftNode); context.report({ fix: fixer, messageId: LUA_TUPLE_DECLARATION, node: leftNode }); } function fixIntoArrayDestructuring(context, node) { const { sourceCode } = context; return (fixer) => { let replacement = `[${node.name}]`; if (node.typeAnnotation) replacement += sourceCode.getText(node.typeAnnotation); return fixer.replaceText(node, replacement); }; } function handleIterableFunction(context, parserServices, node, type) { if (!isTypeReference(type)) return; const checker = parserServices.program.getTypeChecker(); const aliasSymbol = checker.getTypeArguments(type)[0]?.aliasSymbol; if (!aliasSymbol || aliasSymbol.escapedName.toString() !== "LuaTuple") return; if (node.left.type === AST_NODE_TYPES.Identifier) { ensureArrayDestructuring(context, parserServices, node.left); return; } if (node.left.type !== AST_NODE_TYPES.VariableDeclaration) return; const variableDeclarator = node.left.declarations[0]; if (variableDeclarator.id.type === AST_NODE_TYPES.Identifier) ensureArrayDestructuring(context, parserServices, variableDeclarator.id); } function isLuaTuple(parserServices, node) { const { aliasSymbol } = getConstrainedTypeAtLocation(parserServices, node); return (aliasSymbol && aliasSymbol.escapedName.toString() === "LuaTuple") ?? false; } function validateAssignmentExpression(context, parserServices, node) { if (!isLuaTuple(parserServices, node.left) && isLuaTuple(parserServices, node.right)) ensureArrayDestructuring(context, parserServices, node.left); } function validateForOfStatement(context, parserServices, node) { const rightNode = node.right; const type = getConstrainedTypeAtLocation(parserServices, rightNode); if (isIterableFunctionType(parserServices.program, type)) handleIterableFunction(context, parserServices, node, type); else checkLuaTupleUsage(context, parserServices, rightNode); } function validateVariableDeclarator(context, parserServices, node) { if (node.init && isLuaTuple(parserServices, node.init)) ensureArrayDestructuring(context, parserServices, node.id); } const misleadingLuaTupleChecks = createEslintRule({ create: create$21, defaultOptions: [], meta: { docs: { description: "Disallow the use of LuaTuple in conditional expressions", recommended: true, requiresTypeChecking: true }, fixable: "code", messages: messages$21, schema: [], type: "problem" }, name: RULE_NAME$21 }); //#endregion //#region src/rules/no-any/rule.ts const RULE_NAME$20 = "no-any"; const ANY_VIOLATION = "any-violation"; const SUGGEST_UNKNOWN = "suggest-unknown"; const messages$20 = { [ANY_VIOLATION]: "Type 'any' is not supported in roblox-ts.", [SUGGEST_UNKNOWN]: "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." }; function create$20(context, [{ fixToUnknown }]) { return { TSAnyKeyword: (node) => { const isKeyofAny = isNodeWithinKeyofAny(node); if (isKeyofAny) return; const fixOrSuggest = { fix: fixToUnknown ? (fixer) => fixer.replaceText(node, "unknown") : null, suggest: [{ fix: (fixer) => fixer.replaceText(node, "unknown"), messageId: SUGGEST_UNKNOWN }] }; context.report({ messageId: ANY_VIOLATION, node, ...fixOrSuggest }); } }; } function isNodeWithinKeyofAny(node) { return node.parent.type === AST_NODE_TYPES.TSTypeOperator && node.parent.operator === "keyof"; } const noAny = createEslintRule({ create: create$20, defaultOptions: [{ fixToUnknown: true }], meta: { defaultOptions: [{ fixToUnknown: true }], docs: { description: "Disallow values of type `any`. Use `unknown` instead", recommended: true, requiresTypeChecking: false }, fixable: "code", hasSuggestions: true, messages: messages$20, schema: [{ additionalProperties: false, properties: { fixToUnknown: { description: "Whether to enable auto-fixing in which the `any` type is converted to the `unknown` type.'", type: "boolean" } }, type: "object" }], type: "problem" }, name: RULE_NAME$20 }); //#endregion //#region src/rules/no-array-pairs/rule.ts function makeViolationText(name$1) { return `Do not use Array<T> with ${name$1}(). Key values will not be shifted from 1-indexed to 0-indexed.`; } const RULE_NAME$19 = "no-array-pairs"; const ARRAY_PAIRS_VIOLATION = "array-pairs-violation"; const ARRAY_IPAIRS_VIOLATION = "array-ipairs-violation"; const messages$19 = { [ARRAY_IPAIRS_VIOLATION]: makeViolationText("ipairs"), [ARRAY_PAIRS_VIOLATION]: makeViolationText("pairs") }; function create$19(context) { const parserServices = getParserServices(context); const checker = parserServices.program.getTypeChecker(); return { "CallExpression[callee.name=\"ipairs\"], CallExpression[callee.name=\"pairs\"]": (node) => { if (node.callee.type !== AST_NODE_TYPES.Identifier || !node.arguments[0]) return; const type = getConstrainedTypeAtLocation(parserServices, node.arguments[0]); if (!isArrayType(checker, type)) return; context.report({ messageId: node.callee.name === "pairs" ? ARRAY_PAIRS_VIOLATION : ARRAY_IPAIRS_VIOLATION, node }); } }; } const noArrayPairs = createEslintRule({ create: create$19, defaultOptions: [], meta: { docs: { description: "Disallow usage of pairs() and ipairs() with Array<T>", recommended: true, requiresTypeChecking: true }, messages: messages$19, schema: [], type: "problem" }, name: RULE_NAME$19 }); //#endregion //#region src/rules/no-enum-merging/rule.ts const RULE_NAME$18 = "no-enum-merging"; const ENUM_MERGING_VIOLATION = "enum-merging-violation"; const messages$18 = { [ENUM_MERGING_VIOLATION]: "Enum merging is not supported in roblox-ts. Declare all members in a single enum." }; function create$18(context) { return { TSEnumDeclaration(node) { const currentScope = context.sourceCode.getScope(node).upper; if (currentScope === null) return; const variable = currentScope.set.get(node.id.name); if (variable === void 0) return; if (variable.defs.length <= 1) return; context.report({ messageId: ENUM_MERGING_VIOLATION, node: node.id }); } }; } const noEnumMerging = createEslintRule({ create: create$18, defaultOptions: [], meta: { docs: { description: "Disallow merging enum declarations", recommended: true, requiresTypeChecking: false }, messages: messages$18, schema: [], type: "problem" }, name: RULE_NAME$18 }); //#endregion //#region src/rules/no-export-assignment-let/rule.ts const RULE_NAME$17 = "no-export-assignment-let"; const EXPORT_VIOLATION = "export-violation"; const messages$17 = { [EXPORT_VIOLATION]: "Cannot use `export =` on a `let` variable!" }; function create$17(context) { return { TSExportAssignment(node) { const { expression } = node; if (expression.type !== AST_NODE_TYPES.Identifier) return; const variable = context.sourceCode.getScope(node).set.get(expression.name); if (variable === void 0) return; const parent = variable.defs[0]?.parent; if (parent && parent.type === AST_NODE_TYPES.VariableDeclaration && parent.kind === "let") context.report({ messageId: EXPORT_VIOLATION, node }); } }; } const noExportAssignableLet = createEslintRule({ create: create$17, defaultOptions: [], meta: { docs: { description: "Disallow using `export =` on a let variable", recommended: true, requiresTypeChecking: false }, messages: messages$17, schema: [], type: "problem" }, name: RULE_NAME$17 }); //#endregion //#region src/rules/no-for-in/rule.ts const RULE_NAME$16 = "no-for-in"; const FOR_IN_VIOLATION = "for-in-violation"; const messages$16 = { [FOR_IN_VIOLATION]: "For-in loops are forbidden because it always types the iterator variable as `string`. Use for-of or array.forEach instead." }; function create$16(context) { return { ForInStatement(node) { context.report({ fix: (fix) => fix.replaceTextRange([node.left.range[1], node.right.range[0]], " of "), messageId: FOR_IN_VIOLATION, node }); } }; } const noForIn = createEslintRule({ create: create$16, defaultOptions: [], meta: { docs: { description: "Disallow iterating with a for-in loop", recommended: true, requiresTypeChecking: false }, fixable: "code", messages: messages$16, schema: [], type: "problem" }, name: RULE_NAME$16 }); //#endregion //#region src/rules/no-function-expression-name/rule.ts const RULE_NAME$15 = "no-function-expression-name"; const FUNCTION_EXPRESSION_VIOLATION = "function-expression-violation"; const messages$15 = { [FUNCTION_EXPRESSION_VIOLATION]: "Function expression names are not supported!" }; function create$15(context) { return { FunctionExpression(node) { const { id } = node; if (id === null) return; const variable = context.sourceCode.getScope(node).set.get(id.name); const referenced = variable?.references.some((ref) => ref.identifier !== id) ?? false; context.report({ fix: referenced ? null : (fixer) => fixer.removeRange([id.range[0] - 1, id.range[1]]), messageId: FUNCTION_EXPRESSION_VIOLATION, node: id }); } }; } const noFunctionExpressionName = createEslintRule({ create: create$15, defaultOptions: [], meta: { docs: { description: "Disallow the use of function expression names", recommended: true, requiresTypeChecking: false }, fixable: "code", messages: messages$15, schema: [], type: "problem" }, name: RULE_NAME$15 }); //#endregion //#region src/rules/no-get-set/rule.ts const RULE_NAME$14 = "no-get-set"; const GET_SET_VIOLATION = "get-set-violation"; const messages$14 = { [GET_SET_VIOLATION]: "Getters and Setters are not supported for performance reasons. Please use a normal method instead." }; function create$14(context) { function checkMethodDefinition(nodes) { for (const node of nodes) if ((node.type === AST_NODE_TYPES.MethodDefinition || node.type === AST_NODE_TYPES.Property) && (node.kind === "get" || node.kind === "set")) context.report({ fix: (fixer) => fixer.removeRange([node.key.range[0] - 1, node.key.range[0]]), messageId: GET_SET_VIOLATION, node }); } return { ClassBody: (node) => { checkMethodDefinition(node.body); }, ObjectExpression: (node) => { checkMethodDefinition(node.properties); } }; } const noGetSet = createEslintRule({ create: create$14, defaultOptions: [], meta: { docs: { description: "Disallow getters and setters", recommended: true, requiresTypeChecking: false }, fixable: "code", messages: messages$14, schema: [], type: "problem" }, name: RULE_NAME$14 }); //#endregion //#region src/rules/no-implicit-self/rule.ts const RULE_NAME$13 = "no-implicit-self"; const COLON_VIOLATION = "violation"; const messages$13 = { [COLON_VIOLATION]: "Enforce the use of `.` instead of `:` for method calls" }; function create$13(context) { return { ["LabeledStatement[body.type='ExpressionStatement'][body.expression.type='CallExpression'],LabeledStatement[body.expression.type='MemberExpression']"]: (node) => { const { sourceCode } = context; const { body, label } = node; const bodyText = sourceCode.getText(body); const labelText = sourceCode.getText(label); const between = sourceCode.text.slice(label.range[1], body.range[0]); if (/^:\s/g.test(between)) return; const fixedText = `${labelText}.${bodyText}`; context.report({ fix(fixer) { return fixer.replaceText(node, fixedText); }, messageId: COLON_VIOLATION, node }); } }; } const noImplicitSelf = createEslintRule({ create: create$13, defaultOptions: [], meta: { docs: { description: "Enforce the use of `.` instead of `:` for method calls", recommended: false, requiresTypeChecking: false }, fixable: "code", hasSuggestions: false, messages: messages$13, schema: [], type: "problem" }, name: RULE_NAME$13 }); //#endregion //#region src/rules/no-invalid-identifier/rule.ts const RULE_NAME$12 = "no-invalid-identifier"; const BANNED_KEYWORDS = new Set([ "and", "elseif", "end", "error", "local", "nil", "not", "or", "repeat", "then", "until" ]); const LUAU_IDENTIFIER_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/; const INVALID_CHARACTERS = "invalid-characters"; const INVALID_IDENTIFIER = "invalid-identifier"; const messages$12 = { [INVALID_CHARACTERS]: "Identifier '{{ identifier }}' contains invalid characters. Only letters, digits, and underscores are allowed.", [INVALID_IDENTIFIER]: "Avoid using '{{ identifier }}' as an identifier, as it is a reserved keyword in Luau." }; function create$12(context) { const { sourceCode } = context; return { [[ "VariableDeclaration", "FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression", "CatchClause", "TSEnumDeclaration", "TSModuleDeclaration" ].join(",")](node) { for (const variable of sourceCode.getDeclaredVariables(node)) validateIdentifier(context, node, variable.name); }, "ClassDeclaration, ClassExpression"(node) { if (node.id?.name !== void 0) validateIdentifier(context, node, node.id.name); }, "ImportDeclaration"(node) { for (const variable of sourceCode.getDeclaredVariables(node)) validateIdentifier(context, node, variable.name, () => isImportAlias(node)); } }; } function isImportAlias(node) { for (const specifier of node.specifiers) if (specifier.type === AST_NODE_TYPES.ImportSpecifier && (specifier.imported.type !== AST_NODE_TYPES.Identifier || specifier.local.name !== specifier.imported.name)) return true; return false; } function isRestricted(name$1) { return BANNED_KEYWORDS.has(name$1) || !LUAU_IDENTIFIER_REGEX.test(name$1); } function validateIdentifier(context, node, name$1, validate) { if (!isRestricted(name$1) || validate?.() === false) return; context.report({ data: { identifier: name$1 }, messageId: BANNED_KEYWORDS.has(name$1) ? INVALID_IDENTIFIER : INVALID_CHARACTERS, node }); } const noInvalidIdentifier = createEslintRule({ create: create$12, defaultOptions: [], meta: { docs: { description: "Disallow the use of Luau reserved keywords as identifiers", recommended: true, requiresTypeChecking: false }, messages: messages$12, schema: [], type: "problem" }, name: RULE_NAME$12 }); //#endregion //#region src/rules/no-namespace-merging/rule.ts const RULE_NAME$11 = "no-namespace-merging"; const NAMESPACE_MERGING_VIOLATION = "namespace-merging-violation"; const messages$11 = { [NAMESPACE_MERGING_VIOLATION]: "Namespace merging is not supported in roblox-ts. Declare all members in a single namespace." }; function checkNamespaceMerging(context, node) { if (shouldSkipNode(node)) return; const variable = getNamespaceVariable(context, node); if (variable === void 0) return; const allTypeOnly = variable.defs.every((definition) => { return definition.node.type === AST_NODE_TYPES.TSModuleDeclaration && isTypeOnlyNamespace(definition.node); }); if (!allTypeOnly) context.report({ messageId: NAMESPACE_MERGING_VIOLATION, node: node.id }); } function create$11(context) { return { "TSModuleDeclaration[global!=true][id.type!='Literal']"(node) { checkNamespaceMerging(context, node); } }; } function getNamespaceVariable(context, node) { const currentScope = context.sourceCode.getScope(node).upper; if (currentScope === null) return void 0; const variable = currentScope.set.get(node.id.name); if (variable === void 0 || variable.defs.length <= 1) return void 0; return variable; } function isTypeOnlyNamespace(node) { if (!node.body) return true; return node.body.body.every((statement) => { if (statement.type === AST_NODE_TYPES.ExportNamedDeclaration) { if (statement.declaration) return statement.declaration.type === AST_NODE_TYPES.TSTypeAliasDeclaration || statement.declaration.type === AST_NODE_TYPES.TSInterfaceDeclaration; return false; } return statement.type === AST_NODE_TYPES.TSTypeAliasDeclaration || statement.type === AST_NODE_TYPES.TSInterfaceDeclaration || statement.type === AST_NODE_TYPES.TSModuleDeclaration; }); } function shouldSkipNode(node) { return node.parent.type === AST_NODE_TYPES.TSModuleDeclaration || node.id.type !== AST_NODE_TYPES.Identifier; } const noNamespaceMerging = createEslintRule({ create: create$11, defaultOptions: [], meta: { docs: { description: "Disallow merging namespace declarations", recommended: true, requiresTypeChecking: false }, messages: messages$11, schema: [], type: "problem" }, name: RULE_NAME$11 }); //#endregion //#region src/rules/no-null/rule.ts const RULE_NAME$10 = "no-null"; const NULL_VIOLATION = "null-violation"; const messages$10 = { [NULL_VIOLATION]: "Usage of 'null' is not allowed. Use 'undefined' instead." }; function create$10(context) { return { Literal(node) { if (node.value === null) replaceNull(context, node); }, TSNullKeyword(node) { replaceNull(context, node); } }; } function replaceNull(context, node) { context.report({ fix: (fixer) => fixer.replaceText(node, "undefined"), messageId: NULL_VIOLATION, node }); } const noNull = createEslintRule({ create: create$10, defaultOptions: [], meta: { docs: { description: "Disallow usage of the `null` keyword", recommended: true, requiresTypeChecking: false }, fixable: "code", messages: messages$10, schema: [], type: "problem" }, name: RULE_NAME$10 }); //#endregion //#region src/rules/no-object-math/rule.ts const RULE_NAME$9 = "no-object-math"; const OBJECT_MATH_VIOLATION = "object-math-violation"; const OTHER_VIOLATION = "other-violation"; const messages$9 = { [OBJECT_MATH_VIOLATION]: "'{{operator}}' is not supported for Roblox DataType math operations. Use .{{method}}() instead.", [OTHER_VIOLATION]: "Cannot use {{operator}} on this Roblox Datatype." }; const operationConstraints = new Map([ ["CFrame", new Map([ ["add", { acceptedTypes: ["Vector3"], allowSwapped: false }], ["mul", { acceptedTypes: ["CFrame", "Vector3"], allowSwapped: false }], ["sub", { acceptedTypes: ["Vector3"], allowSwapped: false }] ])], ["UDim2", new Map([["add", { acceptedTypes: "same", allowSwapped: false }], ["sub", { acceptedTypes: "same", allowSwapped: false }]])], ["UDim", new Map([["add", { acceptedTypes: "same", allowSwapped: false }], ["sub", { acceptedTypes: "same", allowSwapped: false }]])], ["Vector2", new Map([ ["add", { acceptedTypes: "same", allowSwapped: false }], ["div", { acceptedTypes: ["Vector2", "number"], allowSwapped: false }], ["mul", { acceptedTypes: ["Vector2", "number"], allowSwapped: true }], ["sub", { acceptedTypes: "same", allowSwapped: false }] ])], ["Vector2int16", new Map([ ["add", { acceptedTypes: "same", allowSwapped: false }], ["div", { acceptedTypes: "same", allowSwapped: false }], ["mul", { acceptedTypes: "same", allowSwapped: false }], ["sub", { acceptedTypes: "same", allowSwapped: false }] ])], ["Vector3", new Map([ ["add", { acceptedTypes: "same", allowSwapped: false }], ["div", { acceptedTypes: ["Vector3", "number"], allowSwapped: false }], ["mul", { acceptedTypes: ["Vector3", "number"], allowSwapped: true }], ["sub", { acceptedTypes: "same", allowSwapped: false }] ])], ["Vector3int16", new Map([ ["add", { acceptedTypes: "same", allowSwapped: false }], ["div", { acceptedTypes: "same", allowSwapped: false }], ["mul", { acceptedTypes: "same", allowSwapped: false }], ["sub", { acceptedTypes: "same", allowSwapped: false }] ])] ]); const unaryOperationSupport = new Map([ ["CFrame", false], ["UDim2", true], ["UDim", true], ["Vector2", true], ["Vector2int16", true], ["Vector3", true], ["Vector3int16", true] ]); const mathOperationToMacroName = new Map([ ["*", "mul"], ["+", "add"], ["-", "sub"], ["/", "div"] ]); const safeOperationSymbols = new Set(["!==", "==="]); function buildMethodCallFix({ fixer, left, macroName, operator, right, sourceCode }) { const textBetween = sourceCode.text.slice(left.range[1], right.range[0]); const { afterOp, beforeOp, hasParentheses } = extractOperatorContext(textBetween, operator); if (hasParentheses) return [fixer.replaceTextRange([left.range[1] + beforeOp.length, right.range[0] - afterOp.length], `.${macroName}(`), fixer.insertTextAfter(right, ")")]; return [fixer.replaceTextRange([left.range[1], right.range[0]], `.${macroName}(`), fixer.insertTextAfter(right, ")")]; } function buildSwappedFix({ fixer, left, macroName, right, sourceCode }) { return [fixer.replaceTextRange([left.range[0], right.range[1]], `${sourceCode.getText(right)}.${macroName}(${sourceCode.getText(left)})`)]; } function buildUnaryFix(fixer, sourceCode, node) { const argumentText = sourceCode.getText(node.argument); return [fixer.replaceText(node, `${argumentText}.mul(-1)`)]; } function checkConstraints(parameters) { const { constraints, otherNode, otherType, thisType } = parameters; return isSameTypeConstraint(constraints, otherType, thisType) || isNumberConstraint(constraints, otherType, otherNode) || isTypeInList(constraints, otherType, otherNode); } function create$9(context) { const parserServices = getParserServices(context); return { "BinaryExpression": (node) => { handleBinaryExpression(node, context, parserServices); }, "UnaryExpression[operator=\"-\"]": (node) => { handleUnaryExpression(node, context, parserServices); } }; } function createOperatorFix(fixContext) { const { context, fixer, macroName, node, shouldSwap = false } = fixContext; const { sourceCode } = context; if (node.type === AST_NODE_TYPES.UnaryExpression) return buildUnaryFix(fixer, sourceCode, node); const { left, operator, right } = node; if (shouldSwap) return buildSwappedFix({ fixer, left, macroName, right, sourceCode }); return buildMethodCallFix({ fixer, left, macroName, operator, right, sourceCode }); } function createValidationResult(dataType, isValid, shouldSwap = false) { return { dataType, isValid, shouldSwap }; } function createViolationContext({ context, macroName, node, operator, validation }) { const violationType = validation.isValid ? "math-operation" : "unsupported"; const violationContext = { context, node, operator, type: violationType }; if (validation.isValid) { violationContext.macroName = macroName; violationContext.operator = operator; violationContext.shouldSwap = validation.shouldSwap; } return violationContext; } function extractOperatorContext(textBetween, operator) { const hasParentheses = textBetween.includes(")") && textBetween.includes(operator); if (!hasParentheses) return { afterOp: "", beforeOp: "", hasParentheses: false }; const operatorIndex = textBetween.indexOf(operator); const beforeOp = textBetween.slice(0, operatorIndex).trimEnd(); const afterOp = textBetween.slice(operatorIndex + operator.length).trimStart(); return { afterOp, beforeOp, hasParentheses: true }; } function getOperationConstraints(operandType, macroName) { return operationConstraints.get(operandType)?.get(macroName); } function getRobloxTypeFromBinaryExpr(node, parserServices) { const { left, operator, right } = node; const macroName = mathOperationToMacroName.get(operator); if (macroName === void 0) return void 0; const leftType = getRobloxTypeName(left, parserServices); const rightType = getRobloxTypeName(right, parserServices); const validation = validateOperation({ leftNode: left, leftType, operator, rightNode: right, rightType }); return validation.isValid ? validation.dataType : void 0; } function getRobloxTypeFromMethodCall(node, parserServices) { if (node.callee.type !== AST_NODE_TYPES.MemberExpression || node.callee.property.type !== AST_NODE_TYPES.Identifier) return void 0; const methodName = node.callee.property.name; const objectType = getSimpleRobloxType(node.callee.object, parserServices); if (objectType !== void 0 && operationConstraints.get(objectType)?.has(methodName) === true) return objectType; return void 0; } function getRobloxTypeName(node, parserServices) { const simpleType = getSimpleRobloxType(node, parserServices); if (simpleType !== void 0) return simpleType; if (node.type === AST_NODE_TYPES.CallExpression) return getRobloxTypeFromMethodCall(node, parserServices); if (node.type === AST_NODE_TYPES.BinaryExpression) return getRobloxTypeFromBinaryExpr(node, parserServices); return void 0; } function getSimpleRobloxType(node, parserServices) { const type = getConstrainedTypeAtLocation(parserServices, node); return getRobloxDataTypeNameRecursive(type); } function handleBinaryExpression(node, context, parserServices) { const { left, operator, right } = node; if (shouldSkipOperation(operator)) return; const leftDataType = getRobloxTypeName(left, parserServices); const rightDataType = getRobloxTypeName(right, parserServices); if (leftDataType === void 0 && rightDataType === void 0) return; processMathOperation({ context, leftDataType, node, operands: { left, operator, right }, rightDataType }); } function handleUnaryExpression(node, context, parserServices) { const argumentDataType = getRobloxTypeName(node.argument, parserServices); if (argumentDataType === void 0) return; const violationType = unaryOperationSupport.get(argumentDataType) === true ? "unary-operation" : "unsupported"; reportViolation({ context, node, operator: node.operator, type: violationType }); } function isNumberConstraint(constraints, otherType, otherNode) { return constraints.acceptedTypes === "number" && (otherType === void 0 || isNumericLiteral(otherNode)); } function isNumericLiteral(node) { return node.type === AST_NODE_TYPES.Literal && typeof node.value === "number"; } function isSameTypeConstraint(constraints, otherType, thisType) { return constraints.acceptedTypes === "same" && otherType === thisType; } function isTypeInList(constraints, otherType, otherNode) { if (!Array.isArray(constraints.acceptedTypes)) return false; if (constraints.acceptedTypes.includes("number") && (otherType === void 0 || isNumericLiteral(otherNode))) return true; return otherType !== void 0 && constraints.acceptedTypes.includes(otherType); } function processMathOperation({ context, leftDataType, node, operands, rightDataType }) { const { left, operator, right } = operands; const macroName = mathOperationToMacroName.get(operator); if (macroName === void 0) { reportViolation({ context, node, operator, type: "unsupported" }); return; } const validation = validateOperation({ leftNode: left, leftType: leftDataType, operator, rightNode: right, rightType: rightDataType }); const violationContext = createViolationContext({ context, macroName, node, operator, validation }); reportViolation(violationContext); } function reportViolation(violationContext) { const { context, macroName, node, operator, shouldSwap = false, type } = violationContext; if (type === "unsupported") { context.report({ messageId: OTHER_VIOLATION, node }); return; } const data = { method: macroName ?? "mul", operator }; context.report({ data, fix: (fixer) => { return createOperatorFix({ context, fixer, macroName: macroName ?? "mul", node, shouldSwap }); }, messageId: OBJECT_MATH_VIOLATION, node }); } function shouldSkipOperation(operator) { return safeOperationSymbols.has(operator); } function tryOperandValidation({ macroName, operandType, otherNode, otherType }) { const constraints = getOperationConstraints(operandType, macroName); if (!constraints) return void 0; const isValid = checkConstraints({ constraints, otherNode, otherType, thisType: operandType }); return isValid ? createValidationResult(operandType, true, false) : void 0; } function trySwappedValidation({ macroName, operandType, otherNode, otherType }) { const constraints = getOperationConstraints(operandType, macroName); if (constraints?.allowSwapped !== true) return void 0; const isValid = checkConstraints({ constraints, otherNode, otherType, thisType: operandType }); return isValid ? createValidationResult(operandType, true, true) : void 0; } function validateOperation(context) { const { leftNode, leftType, operator, rightNode, rightType } = context; const macroName = mathOperationToMacroName.get(operator); if (macroName === void 0) return createValidationResult(void 0, false); if (leftType !== void 0) { const result = tryOperandValidation({ macroName, operandType: leftType, otherNode: rightNode, otherType: rightType }); if (result) return result; } if (rightType !== void 0) { const result = trySwappedValidation({ macroName, operandType: rightType, otherNode: leftNode, otherType: leftType }); if (result) return result; } return createValidationResult(void 0, false); } const noObjectMath = createEslintRule({ create: create$9, defaultOptions: [], meta: { docs: { description: "Enforce DataType math methods over operators", recommended: true, requiresTypeChecking: true }, fixable: "code", hasSuggestions: false, messages: messages$9, schema: [], type: "problem" }, name: RULE_NAME$9 }); //#endregion //#region src/rules/no-post-fix-new/rule.ts const RULE_NAME$8 = "no-post-fix-new"; const NEW_VIOLATION = "new-violation"; const messages$8 = { [NEW_VIOLATION]: "Calling .new() on objects without a .new() method is probably a mistake. Use `new X()` instead." }; function create$8(context) { const parserServices = getParserServices(context); const checker = parserServices.program.getTypeChecker(); return { CallExpression(node) { handleCallExpression(node, context, parserServices, checker); } }; } function handleCallExpression(node, context, parserServices, checker) { const propertyAccess = parserServices.esTreeNodeToTSNodeMap.get(node.callee); if (!isPropertyAccessExpression(propertyAccess) || propertyAccess.name.text !== "new") return; const objectType = checker.getTypeAtLocation(propertyAccess.expression); const hasNewProperty = objectType.getProperty("new"); const hasNewMethod = hasNewProperty && isFunction(checker.getTypeOfSymbolAtLocation(hasNewProperty, propertyAccess.expression)); if (!(hasNewMethod ?? false)) replaceWithNewExpression(context, node); } function replaceWithNewExpression(context, node) { context.report({ fix: (fixer) => { const { sourceCode } = context; const accessNode = node.callee; const exprText = sourceCode.getText(accessNode.object); const argsText = sourceCode.getText().slice(accessNode.range[1], node.range[1]); const shouldWrap = !(accessNode.object.type === AST_NODE_TYPES.Identifier || accessNode.object.type === AST_NODE_TYPES.MemberExpression && !accessNode.object.computed); const replaced = shouldWrap ? `new (${exprText})${argsText}` : `new ${exprText}${argsText}`; return [fixer.replaceText(node, replaced)]; }, messageId: NEW_VIOLATION, node }); } const noPostFixNew = createEslintRule({ create: create$8, defaultOptions: [], meta: { docs: { description: "Disallow .new() on objects without a .new() method", recommended: true, requiresTypeChecking: true }, fixable: "code", messages: messages$8, schema: [], type: "problem" }, name: RULE_NAME$8 }); //#endregion //#region src/rules/no-preceding-spread-element/rule.ts const RULE_NAME$7 = "no-preceding-spread-element"; const PRECEDING_SPREAD_VIOLATION = "preceding-rest-violation"; const messages$7 = { [PRECEDING_SPREAD_VIOLATION]: "Spread element must come last in a list of arguments!" }; function create$7(context) { const parserServices = getParserServices(context); return { SpreadElement(node) { const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); const { parent } = tsNode; if (!isArrayLiteralExpression(parent) && !isObjectLiteralExpression(parent) && parent.arguments && parent.arguments[parent.arguments.length - 1] !== tsNode) context.report({ messageId: PRECEDING_SPREAD_VIOLATION, node }); } }; } const noPrecedingSpreadElement = createEslintRule({ create: create$7, defaultOptions: [], meta: { docs: { description: "Disallow spread elements not last in a list of arguments", recommended: true, requiresTypeChecking: true }, messages: messages$7, schema: [], type: "problem" }, name: RULE_NAME$7 }); //#endregion //#region src/rules/no-private-identifier/rule.ts const RULE_NAME$6 = "no-private-identifier"; const PRIVATE_IDENTIFIER_VIOLATION = "private-identifier-violation"; const messages$6 = { [PRIVATE_IDENTIFIER_VIOLATION]: "Private identifiers (`#`) are not supported in roblox-ts. Use the 'private' access modifier instead." }; function create$6(context) { return { PrivateIdentifier(node) { context.report({ fix: (fixer) => fixer.replaceText(node, `private ${node.name}`), messageId: PRIVATE_IDENTIFIER_VIOLATION, node }); } }; } const noPrivateIdentifier = createEslintRule({ create: create$6, defaultOptions: [], meta: { docs: { description: "Disallow the use of private identifiers (`#`)", recommended: true, requiresTypeChecking: false }, fixable: "code", messages: messages$6, schema: [], type: "problem" }, name: RULE_NAME$6 }); //#endregion //#region src/rules/no-unsupported-syntax/rule.ts const RULE_NAME$5 = "no-unsupported-syntax"; const GLOBAL_THIS_VIOLATION = "global-this-violation"; const LABEL_VIOLATION = "label-violation"; const PROTOTYPE_VIOLATION = "prototype-violation"; const REGEX_LITERAL_VIOLATION = "regex-literal-violation"; const SPREAD_DESTRUCTURING_VIOLATION = "spread-destructuring-violation"; const messages$5 = { [GLOBAL_THIS_VIOLATION]: "`globalThis` is not supported in roblox-ts.", [LABEL_VIOLATION]: "`label` is not supported in roblox-ts.", [PROTOTYPE_VIOLATION]: "`.prototype` is not supported in roblox-ts.", [REGEX_LITERAL_VIOLATION]: "Regex literals are not supported in roblox-ts", [SPREAD_DESTRUCTURING_VIOLATION]: "Operator `...` is not supported for destructuring!" }; function create$5(context) { return { ArrayPattern: (node) => { reportInvalidSpreadDestructure(context, node); }, Identifier: (node) => { reportGlobalThisViolation(context, node); }, LabeledStatement: (node) => { reportInvalidLabeledStatement(context, node); }, Literal: (node) => { reportRegexViolation(context, node); }, MemberExpression: (node) => { reportPrototypeViolation(context, node); }, ObjectPattern: (node) => { reportInvalidSpreadDestructure(context, node); } }; } function reportGlobalThisViolation(context, node) { if (node.name === "globalThis") context.report({ messageId: GLOBAL_THIS_VIOLATION, node }); } function reportInvalidLabeledStatement(context, node) { context.report({ messageId: LABEL_VIOLATION, node }); } function reportInvalidSpreadDestructure(context, node) { const members = node.type === AST_NODE_TYPES.ArrayPattern ? node.elements : node.properties; for (const member of members) if (member?.type === AST_NODE_TYPES.RestElement) context.report({ messageId: SPREAD_DESTRUCTURING_VIOLATION, node: member }); } function reportPrototypeViolation(context, node) { if (node.property.type === AST_NODE_TYPES.Identifier && node.property.name === "prototype" && !node.computed) context.report({ messageId: PROTOTYPE_VIOLATION, node: node.property }); } function reportRegexViolation(context, node) { const token = context.sourceCode.getFirstToken(node); if (token && token.type === AST_TOKEN_TYPES.RegularExpression) context.report({ messageId: REGEX_LITERAL_VIOLATION, node }); } const noUnsupportedSyntax = createEslintRule({ create: create$5, defaultOptions: [], meta: { docs: { description: "Disallow unsupported syntax in roblox-ts", recommended: true, requiresTypeChecking: false }, messages: messages$5, schema: [], type: "problem" }, name: RULE_NAME$5 }); //#endregion //#region src/rules/no-user-defined-lua-tuple/rule.ts const RULE_NAME$4 = "no-user-defined-lua-tuple"; const LUA_TUPLE_VIOLATION = "lua-tuple-violation"; const MACRO_VIOLATION = "tuple-macro-violation"; const messages$4 = { [LUA_TUPLE_VIOLATION]: "Disallow usage of the LuaTuple type keyword", [MACRO_VIOLATION]: "Disallow usage of the $tuple(...) call" }; function create$4(context) { const { allowTupleMacro = false, shouldFix = true } = context.options[0] ?? {}; return { ...!allowTupleMacro && { "CallExpression[callee.type=\"Identifier\"][callee.name=\"$tuple\"]"(node) { report(context, node.callee, MACRO_VIOLATION, (fixer) => fixTupleMacroCall(node, context, fixer)); } }, "TSInterfaceDeclaration[id.name=\"LuaTuple\"]": (node) => { report(context, node.id, LUA_TUPLE_VIOLATION); }, "TSTypeAliasDeclaration[id.name=\"LuaTuple\"]": (node) => { report(context, node.id, LUA_TUPLE_VIOLATION); }, "TSTypeReference[typeName.name=\"LuaTuple\"][typeName.type=\"Identifier\"]": (node) => { report(context, node.typeName, LUA_TUPLE_VIOLATION, shouldFix ? (fixer) => fixLuaTupleType(node, context, fixer) : null); } }; } function fixLuaTupleType(node, context, fixer) { const typeArgumentNode = node.typeArguments?.params[0]; if (!typeArgumentNode) return null; const { sourceCode } = context; const typeArgumentText = sourceCode.getText(typeArgumentNode); const { parent } = node; if (parent.type === TSESTree.AST_NODE_TYPES.TSAsExpression && parent.typeAnnotation === node) { const asExpression = parent; return fixer.replaceTextRange([asExpression.expression.range[1], asExpression.range[1]], ""); } return fixer.replaceText(node, typeArgumentText); } function fixTupleMacroCall(node, context, fixer) { const { arguments: args } = node; if (args.length === 0) return fixer.replaceText(node, "[]"); const { sourceCode } = context; const tupleElements = args.map((argument) => sourceCode.getText(argument)).join(", "); const replacementText = `[${tupleElements}]`; return fixer.replaceText(node, replacementText); } function report(context, node, messageId, fix = null) { context.report({ fix, messageId, node }); } const noUserDefinedLuaTuple = createEslintRule({ create: create$4, defaultOptions: [{ allowTupleMacro: false, shouldFix: true }], meta: { defaultOptions: [{ allowTupleMacro: false, shouldFix: true }], docs: { description: "Disallow usage of LuaTuple type keyword and $tuple() calls", recommended: true, requiresTypeChecking: false }, fixable: "code", hasSuggestions: false, messages: messages$4, schema: [{ additionalProperties: false, properties: { allowTupleMacro: { default: false, description: "Whether to allow the $tuple(...) macro call", type: "boolean" }, shouldFix: { default: true, description: "Whether to enable auto-fixing in which the `LuaTuple` type is converted to a native TypeScript tuple type", type: "boolean" } }, type: "object" }], type: "suggestion" }, name: RULE_NAME$4 }); //#endregion //#region src/rules/no-value-typeof/rule.ts const RULE_NAME$3 = "no-value-typeof"; const TYPEOF_VALUE_VIOLATION = "typeof-value-violation"; const messages$3 = { [TYPEOF_VALUE_VIOLATION]: "'typeof' operator is not supported! Use `typeIs(value, type)` or `typeOf(value)` instead." }; function create$3(context) { return { UnaryExpression(node) { if (node.operator === "typeof") context.report({ messageId: TYPEOF_VALUE_VIOLATION, node }); } }; } const noValueTypeof = createEslintRule({ create: create$3, defaultOptions: [], meta: { docs: { description: "Disallow using `typeof` to check for value types", recommended: true, requiresTypeChecking: false }, messages: messages$3, schema: [], type: "problem" }, name: RULE_NAME$3 }); //#endregion //#region src/rules/prefer-get-players/rule.ts const RULE_NAME$2 = "prefer-get-players"; const MESSAGE_ID = "get-players-children-violation"; const messages$2 = { [MESSAGE_ID]: "Use Players.GetPlayers() instead of Players.GetChildren() for more accurate types." }; function check(context, node, callee) { context.report({ fix: (fixer) => fixer.replaceText(callee.property, "GetPlayers"), messageId: MESSAGE_ID, node }); } function create$2(context, [{ validateType }]) { return { CallExpression(node) { const { callee } = node; if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.property.type !== AST_NODE_TYPES.Identifier || callee.property.name !== "GetChildren") return; if (validateType) isPlayersCallExpressionType(context, node, callee); else isPlayersCallExpression(context, node, callee); } }; } function isPlayersCallExpression(context, node, callee) { if (callee.object.type !== AST_NODE_TYPES.Identifier || callee.object.name !== "Players") return; check(context, node, callee); } function isPlayersCallExpressionType(context, node, callee) { const parserServices = getParserServices(context); const type = getConstrainedTypeAtLocation(parserServices, callee.object); const isPlayersType = isBuiltinSymbolLike(parserServices.program, type, ["Players"]); if (!isPlayersType) return; const hasGetPlayersProperty = type.getProperty("GetPlayers"); if (!hasGetPlayersProperty) return; check(context, node, callee); } const preferGetPlayers = createEslintRule({ create: create$2, defaultOptions: [{ validateType: false }], meta: { defaultOptions: [{ validateType: false }], docs: { description: "Enforces the use of Players.GetPlayers() instead of Players.GetChildren()", recommended: false, requiresTypeChecking: false