npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pobammer-ts/eslint-cease-nonsense-rules

v1.34.0

Published

A curated ESLint plugin providing rules to catch common developer missteps and enforce safer coding patterns.

Downloads

1,563

Readme

eslint-cease-nonsense-rules

An ESLint plugin that catches common mistakes before they reach production. This collection of rules helps prevent patterns that lead to bugs, performance issues, and maintainability problems.

Table of Contents

Installation

bun add -D @pobammer-ts/eslint-cease-nonsense-rules

Usage

Basic Setup

Add the plugin to your ESLint configuration:

import ceaseNonsense from "@pobammer-ts/eslint-cease-nonsense-rules";

export default [
	{
		plugins: {
			"cease-nonsense": ceaseNonsense,
		},
		rules: {
			// Enable all rules (recommended)
			"cease-nonsense/ban-instances": "error",
			"cease-nonsense/ban-react-fc": "error",
			"cease-nonsense/dot-notation": "error",
			"cease-nonsense/enforce-ianitor-check-type": "error",
			"cease-nonsense/fast-format": "error",
			"cease-nonsense/no-array-size-assignment": "error",
			"cease-nonsense/no-async-constructor": "error",
			"cease-nonsense/no-color3-constructor": "error",
			"cease-nonsense/no-commented-code": "error",
				"cease-nonsense/no-god-components": "error",
				"cease-nonsense/no-identity-map": "error",
				"cease-nonsense/no-instance-methods-without-this": "error",
				"cease-nonsense/no-memo-children": "error",
				"cease-nonsense/no-new-instance-in-use-memo": "error",
				"cease-nonsense/no-underscore-react-props": "error",
				"cease-nonsense/no-print": "error",
				"cease-nonsense/prefer-enum-item": "error",
			"cease-nonsense/prefer-enum-member": "error",
			"cease-nonsense/prefer-idiv": "error",
			"cease-nonsense/no-shorthand-names": "error",
			"cease-nonsense/no-unused-imports": "error",
			"cease-nonsense/no-unused-use-memo": "error",
			"cease-nonsense/no-useless-use-memo": "error",
			"cease-nonsense/no-useless-use-spring": "error",
			"cease-nonsense/no-warn": "error",
			"cease-nonsense/prefer-class-properties": "error",
			"cease-nonsense/prefer-early-return": "error",
			"cease-nonsense/prefer-module-scope-constants": "error",
			"cease-nonsense/prefer-pascal-case-enums": "error",
			"cease-nonsense/prefer-pattern-replacements": "error",
			"cease-nonsense/prefer-sequence-overloads": "error",
			"cease-nonsense/prefer-singular-enums": "error",
			"cease-nonsense/prefer-ternary-conditional-rendering": "error",
			"cease-nonsense/prefer-udim2-shorthand": "error",
			"cease-nonsense/react-hooks-strict-return": "error",
			"cease-nonsense/require-module-level-instantiation": "error",
			"cease-nonsense/require-named-effect-functions": "error",
			"cease-nonsense/require-paired-calls": "error",
			"cease-nonsense/require-react-component-keys": "error",
			"cease-nonsense/require-react-display-names": "error",
			"cease-nonsense/prefer-read-only-props": "error",
			"cease-nonsense/strict-component-boundaries": "error",
			"cease-nonsense/use-exhaustive-dependencies": "error",
			"cease-nonsense/use-hook-at-top-level": "error",
		},
	},
];

Using the Preset

export default [ceaseNonsense.configs.recommended];

Rules

Type Safety

enforce-ianitor-check-type

Enforces Ianitor.Check<T> type annotations on complex TypeScript types to ensure runtime validation.

Calculates structural complexity of types and requires Ianitor validators when complexity exceeds thresholds.

Configuration

{
  "cease-nonsense/enforce-ianitor-check-type": ["error", {
    "baseThreshold": 10,      // Minimum complexity to require validation
    "warnThreshold": 15,      // Warning threshold
    "errorThreshold": 25,     // Error threshold
    "interfacePenalty": 20,   // Complexity penalty for interfaces
    "performanceMode": true   // Enable performance optimizations
  }]
}

❌ Bad

// Complex type without runtime validation
type UserConfig = {
	id: number;
	name: string;
	settings: {
		theme: string;
		notifications: boolean;
	};
};

const config = getUserConfig(); // No runtime check!

✅ Good

const userConfigValidator = Ianitor.interface({
	id: Ianitor.number(),
	name: Ianitor.string(),
	settings: Ianitor.interface({
		theme: Ianitor.string(),
		notifications: Ianitor.boolean(),
	}),
});

type UserConfig = Ianitor.Static<typeof userConfigValidator>;

const config = userConfigValidator.check(getUserConfig());

prefer-enum-item

Enforce using EnumItem values instead of string or number literals when the type expects an EnumItem. Provides type safety and avoids magic values in roblox-ts code.

Configuration

{
  "cease-nonsense/prefer-enum-item": ["error", {
    "fixNumericToValue": false,  // When true, numbers fix to Enum.X.Y.Value
    "performanceMode": false     // When true, cache enum lookups
  }]
}

❌ Bad

// Magic string
<uiflexitem FlexMode="Fill" />

// Magic number
const props: ImageProps = { ScaleType: 1 };

✅ Good

// Explicit enum
<uiflexitem FlexMode={Enum.UIFlexMode.Fill} />

// Explicit enum in object
	const props: ImageProps = { ScaleType: Enum.ScaleType.Slice };

prefer-enum-member

Enforce using enum member references instead of raw string or number values when the type expects a TypeScript enum. Covers both enum and const enum, including object literal keys used with mapped types like Record<Color, T>.

Configuration

{
  "cease-nonsense/prefer-enum-member": "error"
}

❌ Bad

enum Color {
	Blue = "Blue",
	Green = "Green",
}

const meta: Record<Color, number> = {
	Blue: 1,
	Green: 2,
};

const selected: Color = "Blue";

✅ Good

enum Color {
	Blue = "Blue",
	Green = "Green",
}

const meta: Record<Color, number> = {
	[Color.Blue]: 1,
	[Color.Green]: 2,
};

const selected: Color = Color.Blue;

prefer-idiv

Prefer using .idiv() for integer division instead of math.floor(x / y) in roblox-ts. The .idiv() method compiles to Luau's // (floor division) operator, which is more idiomatic and efficient.

Configuration

{
  "cease-nonsense/prefer-idiv": "error"
}

❌ Bad

const quotient = math.floor(x / y);
const result = math.floor((a + b) / c);
const value = math.floor(100 / 3);

✅ Good

const quotient = x.idiv(y);
const result = (a + b).idiv(c);
const value = (100).idiv(3);
const nested = (a / b).idiv(c);

require-serialized-numeric-data-type

Require specific serialized numeric data types (DataType.*) instead of generic number for ECS components and other serialization contexts.

Configuration

{
  "cease-nonsense/require-serialized-numeric-data-type": ["error", {
    "mode": "type-arguments",           // "type-arguments" (default) or "all"
    "functionNames": ["registerComponent"],  // Functions to check in type-arguments mode
    "strict": false                     // Enable type checker resolution for aliases
  }]
}

❌ Bad

// Generic number type
export const Wave = registerComponent<number>({ replicated: true });

// Object with number property
export const WaveTime = registerComponent<{ elapsed: number }>({ replicated: true });

✅ Good

import type { DataType } from "@rbxts/flamework-binary-serializer";

// Specific DataType variants
export const Wave = registerComponent<DataType.u8>({ replicated: true });
export const WaveTime = registerComponent<DataType.f32>({ replicated: true });
export const Yen = registerComponent<DataType.u32>({ replicated: true });

// Object with DataType properties
export const Position = registerComponent<{
	x: DataType.f32;
	y: DataType.f32;
	z: DataType.f32;
}>({ replicated: true });

no-unused-imports

Disallow unused imports. Uses ESLint's scope analysis to detect unused imports and optionally checks JSDoc comments for references.

Configuration

{
  "cease-nonsense/no-unused-imports": ["error", {
    "checkJSDoc": true  // Check if imports are referenced in JSDoc comments
  }]
}

Features

  • ✨ Has auto-fix
  • Cached JSDoc comment parsing per file
  • Pre-compiled regex patterns
  • Efficient scope analysis using ESLint's built-in mechanisms

❌ Bad

import { unusedFunction } from "./utils";
import { AnotherUnused } from "./types";

// unusedFunction and AnotherUnused are never used

✅ Good

import { usedFunction } from "./utils";

usedFunction();

// Or used in JSDoc
/**
 * @see {usedFunction}
 */

React

ban-react-fc

Bans React.FC and similar component type annotations. Use explicit function declarations instead.

React.FC types break debug information in React DevTools and encourage poor patterns.

❌ Bad

export const MyComponent: React.FC<Props> = ({ children }) => {
  return <div>{children}</div>;
};

const Button: FC<ButtonProps> = ({ label }) => <button>{label}</button>;

const Modal: React.FunctionComponent = () => <div>Modal</div>;

const Input: VFC = () => <input />;

✅ Good

export function MyComponent({ children }: Props) {
  return <div>{children}</div>;
}

function Button({ label }: ButtonProps) {
  return <button>{label}</button>;
}

function Modal() {
  return <div>Modal</div>;
}

function Input() {
  return <input />;
}

no-memo-children

Disallow React.memo on components that accept a children prop, since children typically change on every render and defeat memoization.

Why

React.memo performs a shallow comparison of props. When a component accepts children, the children prop is typically a new JSX element on every parent render. This causes the shallow comparison to fail every time, making memo useless while adding overhead.

Configuration

{
  "cease-nonsense/no-memo-children": ["error", {
    "allowedComponents": ["Modal", "Drawer"],  // Allow specific components
    "environment": "roblox-ts"                  // or "standard"
  }]
}

❌ Bad

import { memo, ReactNode } from "@rbxts/react";

interface CardProps {
	readonly title: string;
	readonly children?: ReactNode;
}

// memo is useless - children change every render
const Card = memo<CardProps>(({ title, children }) => {
	return (
		<frame>
			<textlabel Text={title} />
			{children}
		</frame>
	);
});

✅ Good

import { memo, ReactNode } from "@rbxts/react";

// Option 1: Remove memo if you need children
interface CardProps {
	readonly title: string;
	readonly children?: ReactNode;
}

function Card({ title, children }: CardProps) {
	return (
		<frame>
			<textlabel Text={title} />
			{children}
		</frame>
	);
}

// Option 2: Use render prop instead of children
interface ListProps<T> {
	readonly items: ReadonlyArray<T>;
	readonly renderItem: (item: T) => ReactNode;
}

const List = memo(<T,>({ items, renderItem }: ListProps<T>) => {
	return <frame>{items.map(renderItem)}</frame>;
});

no-unused-use-memo

Disallow standalone useMemo calls that ignore the memoized value.

Why

useMemo is for deriving values. When you call it as a standalone statement, you are running side effects in the wrong hook and often trying to bypass effect-related rules. Use useEffect for side effects.

Configuration

{
  "cease-nonsense/no-unused-use-memo": ["error", {
    "environment": "roblox-ts" // or "standard"
  }]
}

❌ Bad

import { useMemo } from "react";

useMemo(() => {
	trackAnalytics();
}, [eventName]);

✅ Good

import { useEffect, useMemo } from "react";

useEffect(() => {
	trackAnalytics();
}, [eventName]);

const config = useMemo(() => buildConfig(eventName), [eventName]);

no-useless-use-memo

Disallow useMemo around values that are already static enough to live at module scope.

Why

useMemo only helps when a value is expensive and depends on changing inputs. If the callback always returns the same value, the hook adds overhead without changing the result. Move that value outside the component instead.

Configuration

{
  "cease-nonsense/no-useless-use-memo": ["error", {
    "dependencyMode": "non-updating", // "empty-or-omitted" | "non-updating" | "aggressive"
    "environment": "roblox-ts",       // or "standard"
    "staticGlobalFactories": ["Color3", "Vector3"]
  }]
}

❌ Bad

import { useMemo } from "react";

const rotationConfiguration = useMemo(
	() => getAnimationConfiguration(SpringConfiguration.Sharp, AnimationLibrary.ReactSpring),
	[],
);

✅ Good

const rotationConfiguration = getAnimationConfiguration(
	SpringConfiguration.Sharp,
	AnimationLibrary.ReactSpring,
);

function Component({ theme }) {
	const config = useMemo(() => buildConfig(theme), [theme]);
	return config;
}

no-new-instance-in-use-memo

Disallow configured constructor calls (default: new Instance(...)) inside useMemo callbacks.

Why

useMemo callbacks should stay pure and deterministic. Allocating objects like Roblox Instances inside useMemo creates resources during render flow and can hide lifecycle work that belongs in an effect or dedicated setup path.

Configuration

{
  "cease-nonsense/no-new-instance-in-use-memo": ["error", {
    "constructors": ["Instance"],     // Defaults to ["Instance"]
    "environment": "roblox-ts",       // or "standard"
    "maxHelperTraceDepth": 4          // Trace depth for local helper calls from useMemo
  }]
}

❌ Bad

import { useMemo } from "@rbxts/react";

const itemModel = useMemo(() => {
	const model = new Instance("Model");
	model.Name = "ItemPreview";
	return model;
}, [itemId]);
import { useMemo } from "@rbxts/react";

function createItemModel(id: string) {
	const model = new Instance("Model");
	model.Name = id;
	return model;
}

const itemModel = useMemo(() => createItemModel(itemId), [itemId]);

✅ Good

import { useEffect, useMemo, useState } from "@rbxts/react";

const [itemModel, setItemModel] = useState<Model | undefined>(undefined);

useEffect(() => {
	const model = new Instance("Model");
	model.Name = "ItemPreview";
	setItemModel(model);

	return () => model.Destroy();
}, [itemId]);

const texture = useMemo(() => lookupTexture(itemId), [itemId]);

no-render-helper-functions

Disallow non-component functions that return JSX or React elements.

Why

Functions that return JSX should be proper React components (PascalCase) with a single props parameter, not camelCase helper functions. Render helpers break component tree visibility in DevTools, don't follow React conventions, and encourage spreading component logic across utility functions instead of composing components properly.

❌ Bad

function createLeftLabel(text: string, gradient: ColorSequence): React.ReactNode {
	return (
		<TextLabel
			nativeProperties={{ Text: text }}
			textGradientNativeProperties={{ Color: gradient }}
		/>
	);
}

function createRightLabel(text: string, rotation: number): React.ReactNode {
	return <TextLabel nativeProperties={{ Text: text, Rotation: rotation }} />;
}

export function TimeChamberRow() {
	return (
		<frame>
			{createLeftLabel("Boost", GRADIENT)}
			{createRightLabel("+50%", 90)}
		</frame>
	);
}

✅ Good

interface LabelProps {
	readonly text: string;
	readonly gradient: ColorSequence;
}

function LeftLabel({ text, gradient }: LabelProps) {
	return (
		<TextLabel
			nativeProperties={{ Text: text }}
			textGradientNativeProperties={{ Color: gradient }}
		/>
	);
}

interface RightLabelProps {
	readonly text: string;
	readonly rotation: number;
}

function RightLabel({ text, rotation }: RightLabelProps) {
	return <TextLabel nativeProperties={{ Text: text, Rotation: rotation }} />;
}

export function TimeChamberRow() {
	return (
		<frame>
			<LeftLabel text="Boost" gradient={GRADIENT} />
			<RightLabel text="+50%" rotation={90} />
		</frame>
	);
}

no-underscore-react-props

Ban React prop names that begin with _.

Why

Props are part of a component's public API. Leading underscores usually imply private/internal fields, which makes prop contracts harder to read and inconsistent across components.

❌ Bad

<InventoryItemTooltip
	key="inventory-tooltip"
	_tooltipGradient={tooltipGradient}
/>

✅ Good

<InventoryItemTooltip
	key="inventory-tooltip"
	tooltipGradient={tooltipGradient}
/>

no-useless-use-effect (Experimental)

Disallow effects that only derive state, reset or adjust state from properties, notify parents, or route event flags.

Why

useEffect is for synchronizing with external systems. If an effect only sets local state based on properties or state, only calls a property callback, or only runs because a state flag was toggled, it adds extra renders without providing real synchronization.

Configuration

{
  "cease-nonsense/no-useless-use-effect": ["error", {
    "environment": "roblox-ts", // or "standard"
    "hooks": ["useEffect", "useLayoutEffect", "useInsertionEffect"],
    "reportDerivedState": true,
    "reportNotifyParent": true,
    "reportEventFlag": true,
    "propertyCallbackPrefixes": ["on"]
  }]
}

❌ Bad

import { useEffect, useState } from "@rbxts/react";

function Profile(properties) {
	const [fullName, setFullName] = useState("");

	useEffect(() => {
		setFullName(`${properties.first} ${properties.last}`);
	}, [properties.first, properties.last]);
}

function Form(properties) {
	useEffect(() => {
		properties.onChange(properties.value);
	}, [properties.value, properties.onChange]);
}

function SubmitButton() {
	const [submitted, setSubmitted] = useState(false);

	useEffect(() => {
		if (!submitted) return;
		submitForm();
		setSubmitted(false);
	}, [submitted]);
}

✅ Good

function Profile(properties) {
  const fullName = `${properties.first} ${properties.last}`;
  return <textlabel Text={fullName} />;
}

function Form(properties) {
  function handleSubmit() {
    properties.onChange(properties.value);
  }

  return <textbutton Event={{ Activated: handleSubmit }} />;
}

function SubmitButton() {
  function handleSubmit() {
    submitForm();
  }

  return <textbutton Event={{ Activated: handleSubmit }} />;
}

no-god-components

Flags React components that are too large or doing too much, encouraging better separation of concerns.

Default thresholds

  • Component body line count: target 120, hard max 200
  • TSX nesting depth ≤ 3
  • Stateful hooks ≤ 5
  • Destructured props in parameters ≤ 5
  • Runtime null literals are always banned

Configuration

{
  "cease-nonsense/no-god-components": ["error", {
    targetLines: 120,
    maxLines: 200,
    maxTsxNesting: 3,
    maxStateHooks: 5,
    stateHooks: ["useState", "useReducer", "useBinding"],
    maxDestructuredProps: 5,
    enforceTargetLines: true,
    ignoreComponents: ["LegacyComponent"]
  }]
}

require-react-component-keys

Enforces key props on all React elements except top-level returns from components.

Configuration

{
  "cease-nonsense/require-react-component-keys": ["error", {
    "allowRootKeys": false,                    // Allow keys on root returns
    "ignoreCallExpressions": ["ReactTree.mount"] // Functions to ignore
  }]
}

❌ Bad

function UserList({ users }) {
  return (
    <div>
      {users.map(user => (
        <UserCard user={user} /> // Missing key!
      ))}
    </div>
  );
}

✅ Good

function UserList({ users }) {
  return (
    <div>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}

require-named-effect-functions

Enforce named effect functions for better debuggability. Prevent inline arrow functions in useEffect and similar hooks.

Behavior by environment

  • roblox-ts (default): Only identifiers allowed (e.g., useEffect(onTick, [...]))
  • standard: Identifiers and named function expressions allowed

Configuration

{
  "cease-nonsense/require-named-effect-functions": ["error", {
    "environment": "roblox-ts", // or "standard"
    "hooks": ["useEffect", "useLayoutEffect", "useInsertionEffect"]
  }]
}

❌ Bad

// Arrow function
useEffect(() => {
	doThing();
}, [dep]);

// Anonymous function expression
useEffect(
	function () {
		doThing();
	},
	[dep],
);

✅ Good

// Preferred: reference a named function
function onDepChange() {
	doThing();
}
useEffect(onDepChange, [dep]);

// Allowed in `standard` mode
useEffect(
	function onDepChange() {
		doThing();
	},
	[dep],
);

use-exhaustive-dependencies

Enforces exhaustive and correct dependency specification in React hooks to prevent stale closures and unnecessary re-renders.

Configuration

{
  "cease-nonsense/use-exhaustive-dependencies": ["error", {
    "reportMissingDependenciesArray": true,
    "reportUnnecessaryDependencies": true,
    "hooks": [
      {
        "name": "useCustomHook",
        "closureIndex": 0,
        "dependenciesIndex": 1,
        "stableResult": true
      }
    ]
  }]
}

❌ Bad

function UserProfile({ userId }) {
	const [user, setUser] = useState(null);

	useEffect(() => {
		fetchUser(userId).then(setUser);
	}, []); // Missing userId dependency!
}

✅ Good

function UserProfile({ userId }) {
	const [user, setUser] = useState(null);

	useEffect(() => {
		fetchUser(userId).then(setUser);
	}, [userId]);
}

no-useless-use-spring

Flags useSpring-style hooks that never change (static config plus non-updating deps).

Configuration

{
  "cease-nonsense/no-useless-use-spring": ["error", {
    "springHooks": ["useSpring", "useMotion"],
    "treatEmptyDepsAsViolation": true
  }]
}

❌ Bad

const spring = useSpring({ opacity: 1 }, []);

✅ Good

const spring = useSpring({ opacity: isOpen ? 1 : 0 }, [isOpen]);

use-hook-at-top-level

Enforces that React hooks are only called at the top level of components or custom hooks, never conditionally or in nested functions.

Configuration

{
  "cease-nonsense/use-hook-at-top-level": ["error", {
    // Strategy 1: Ignore hooks by name
    "ignoreHooks": ["useEntity", "useComponent"],

    // Strategy 2: Control by import source
    "importSources": {
      "react": true,           // Check hooks from React
      "my-ecs-library": false  // Ignore ECS hooks
    },

    // Strategy 3: Whitelist mode
    "onlyHooks": ["useState", "useEffect", "useContext"]
  }]
}

❌ Bad

function UserProfile({ userId }) {
	if (userId) {
		useEffect(() => {
			// Hook in conditional!
			fetchUser(userId);
		}, [userId]);
	}
}

✅ Good

function UserProfile({ userId }) {
	useEffect(() => {
		if (userId) {
			fetchUser(userId);
		}
	}, [userId]);
}

require-react-display-names

Require displayName property on exported React.memo components and React.createContext contexts for better debugging.

Configuration

{
  "cease-nonsense/require-react-display-names": ["error", {
    "environment": "roblox-ts"  // or "standard" for regular React
  }]
}

❌ Bad

import { memo } from "@rbxts/react";

function CoolFrameNoMemo() {
	return <frame />;
}

// Direct export without displayName
export default memo(CoolFrameNoMemo);
import React from "@rbxts/react";

// Missing displayName
const ErrorBoundaryContext = React.createContext<unknown>(undefined);
export default ErrorBoundaryContext;

✅ Good

import { memo } from "@rbxts/react";

function CoolFrameNoMemo() {
	return <frame />;
}

export const CoolFrame = memo(CoolFrameNoMemo);
CoolFrame.displayName = "CoolFrame";
export default CoolFrame;
import React from "@rbxts/react";

const ErrorBoundaryContext = React.createContext<unknown>(undefined);
ErrorBoundaryContext.displayName = "ErrorBoundaryContext";
export default ErrorBoundaryContext;

prefer-context-stack

Prefer a local ContextStack component over directly nesting multiple context providers.

This rule only reports direct provider wrapper chains, and only when the project already defines a local ContextStack component.

❌ Bad

return (
	<ThemeContext.Provider value={theme}>
		<LocaleContext.Provider value={locale}>
			<App />
		</LocaleContext.Provider>
	</ThemeContext.Provider>
);

✅ Good

return (
	<ContextStack
		providers={[
			<ThemeContext.Provider value={theme} />,
			<LocaleContext.Provider value={locale} />,
		]}
	>
		<App />
	</ContextStack>
);

prefer-local-portal-component

Prefer a local Portal wrapper component over direct createPortal(...) calls.

This rule only reports when it can find a local Portal component in the project. Auto-fix only runs when that component is already imported in the current file.

❌ Bad

import { createPortal } from "@rbxts/react-roblox";

return createPortal(<frame />, target);

✅ Good

import Portal from "../components/portal";

return <Portal target={target}><frame /></Portal>;

prefer-padding-components

Prefer EqualPadding or DirectionalPadding over a raw <uipadding /> when the padding values already match those abstractions.

This rule only reports when it can find the matching local component in the project.

For DirectionalPadding, this rule follows the local component contract you provide: horizontal maps to PaddingTop/PaddingBottom, and vertical maps to PaddingLeft/PaddingRight.

❌ Bad

<uipadding
	PaddingBottom={padding}
	PaddingLeft={padding}
	PaddingRight={padding}
	PaddingTop={padding}
/>

✅ Good

<EqualPadding padding={padding} />

<DirectionalPadding horizontal={horizontal} vertical={vertical} />

prefer-read-only-props

Enforce that React component props are typed as readonly in TypeScript, preventing accidental mutation of props.

Features

  • ✨ Has auto-fix
  • Direct AST pattern matching (no global component analysis)
  • Component detection caching
  • Focused on common React component patterns

❌ Bad

interface Props {
	name: string;
	age: number;
}

function Component({ name, age }: Props) {
	// ...
}

✅ Good

interface Props {
	readonly name: string;
	readonly age: number;
}

function Component({ name, age }: Props) {
	// ...
}

prefer-ternary-conditional-rendering

Prefer a single ternary expression when two JSX && branches are complements.

Configuration

{
  "cease-nonsense/prefer-ternary-conditional-rendering": "error"
}

❌ Bad

function Component({ gradient, gradientToUse, rarityStyle }) {
	return (
		<>
			{gradient !== undefined && <uigradient key="ui-gradient" Color={gradient} />}
			{gradient === undefined && (
				<AnimatedGradient
					key="animated-gradient"
					colorValue={gradientToUse}
					rotation={45}
					sweepingSpeed={rarityStyle?.sweepingSpeed ?? 0}
				/>
			)}
		</>
	);
}

✅ Good

function Component({ gradient, gradientToUse, rarityStyle }) {
	return (
		<>
			{gradient !== undefined ? (
				<uigradient key="ui-gradient" Color={gradient} />
			) : (
				<AnimatedGradient
					key="animated-gradient"
					colorValue={gradientToUse}
					rotation={45}
					sweepingSpeed={rarityStyle?.sweepingSpeed ?? 0}
				/>
			)}
		</>
	);
}

react-hooks-strict-return

React hooks must return a tuple of ≤2 elements or a single object. Prevents unwieldy hook return types.

❌ Bad

function useMyHook() {
	return [a, b, c]; // 3+ elements
}

function useData() {
	const items = [1, 2, 3];
	return items; // Variable reference to 3+ element array
}

✅ Good

function useMyHook() {
	return [state, setState]; // 2 elements max
}

function useData() {
	return { a, b, c }; // Objects are fine regardless of size
}

Logging

no-print

Bans use of print() function calls. Use Log instead.

❌ Bad

print("Debug message");

✅ Good

Log.info("Debug message");

no-warn

Bans use of warn() function calls. Use Log instead.

❌ Bad

warn("Warning message");

✅ Good

Log.warn("Warning message");

Resource Management

require-paired-calls

Enforces that paired function calls (opener/closer) are properly balanced across all execution paths with LIFO ordering.

Configuration

{
  "cease-nonsense/require-paired-calls": ["error", {
    "pairs": [{
      "opener": "debug.profilebegin",
      "closer": "debug.profileend",
      "alternatives": ["db.rollback"],
      "requireSync": true,
      "platform": "roblox",
      "yieldingFunctions": [
        "task.wait",
        "*.WaitForChild"
      ]
    }],
    "allowConditionalClosers": false,
    "allowMultipleOpeners": true,
    "maxNestingDepth": 0
  }]
}

Pair configuration options

  • opener (required): Function name that starts the paired operation
  • openerAlternatives (optional): Additional opener names sharing the same closer
  • closer (required): Function name(s) that close the operation
  • alternatives (optional): Alternative closers for error paths
  • requireSync (optional): Disallow await/yield between opener and closer
  • platform (optional): Enables "roblox"-specific behavior
  • yieldingFunctions (optional): Custom patterns for Roblox yielding functions (supports wildcards)

Top-level options

  • pairs (required): Array of pair configurations
  • allowConditionalClosers (optional): Allow closers in some but not all branches
  • allowMultipleOpeners (optional): Allow consecutive opener calls
  • maxNestingDepth (optional): Maximum nesting depth (0 = unlimited)

Default configuration (Roblox profiling)

{
  "cease-nonsense/require-paired-calls": ["error", {
    "pairs": [{
      "opener": "debug.profilebegin",
      "closer": "debug.profileend",
      "platform": "roblox",
      "requireSync": true,
      "yieldingFunctions": ["task.wait", "wait", "*.WaitForChild"]
    }]
  }]
}

❌ Bad

// Missing closer on early return
function test() {
	debug.profilebegin("task");
	if (error) return; // Never closed on this path
	debug.profileend();
}

// Wrong LIFO order
function test() {
	debug.profilebegin("outer");
	debug.profilebegin("inner");
	debug.profileend(); // closes inner
	// outer is never closed
}

// Async operation with requireSync
async function test() {
	debug.profilebegin("task");
	await fetch("/api");
	debug.profileend();
}

✅ Good

// Simple pairing
function test() {
	debug.profilebegin("task");
	doWork();
	debug.profileend();
}

// Proper LIFO nesting
function test() {
	debug.profilebegin("outer");
	debug.profilebegin("inner");
	debug.profileend();
	debug.profileend();
}

// Try-finally ensures closer on all paths
function test() {
	debug.profilebegin("task");
	try {
		riskyOperation();
	} finally {
		debug.profileend();
	}
}

Real-world examples

// Database transactions
{
  "pairs": [{
    "opener": "db.transaction",
    "closer": "db.commit",
    "alternatives": ["db.rollback"]
  }]
}

// Lock acquire/release
{
  "pairs": [{
    "opener": "lock.acquire",
    "closer": ["lock.release", "lock.free"]
  }]
}

// Roblox Iris widgets
{
  "pairs": [{
    "opener": "Iris.CollapsingHeader",
    "openerAlternatives": ["Iris.Window", "Iris.TreeNode", "Iris.Table"],
    "closer": "Iris.End",
    "platform": "roblox",
    "requireSync": true
  }]
}

Code Quality

ban-instances

Bans specified Roblox Instance classes in new Instance() calls and JSX elements.

Configuration

// Array format (default message)
{
  "cease-nonsense/ban-instances": ["error", {
    "bannedInstances": ["Part", "Script", "LocalScript"]
  }]
}

// Object format (custom messages)
{
  "cease-nonsense/ban-instances": ["error", {
    "bannedInstances": {
      "Part": "Use MeshPart instead for better performance",
      "Script": "Scripts should not be created at runtime"
    }
  }]
}

❌ Bad

// With config: { bannedInstances: ["Part", "Script"] }
const part = new Instance("Part");
const script = new Instance("Script");

// JSX (lowercase = Roblox Instance)
<part Size={new Vector3(1, 1, 1)} />

✅ Good

const meshPart = new Instance("MeshPart");
<meshpart Size={new Vector3(1, 1, 1)} />

no-constant-condition-with-break

Disallows constant conditions, but allows constant loops that include exits like break, return, or configured exit calls. This is useful for game loops and event loops that have intentional exit conditions.

Configuration

{
  "cease-nonsense/no-constant-condition-with-break": "error"
}
{
  "cease-nonsense/no-constant-condition-with-break": ["error", {
    loopExitCalls: ["coroutine.yield", "task.wait"]
  }]
}

❌ Bad

// Constant condition in if statement
if (true) { doSomething(); }

// Infinite loop without a loop exit
while (true) { doSomething(); }
for (;; true) { doSomething(); }

✅ Good

// Variable condition
while (condition) { doSomething(); }

// Loop with proper break
while (true) {
  if (shouldStop) break;
  doSomething();
}

// Labeled break
outer: while (true) {
  if (done) break outer;
  doSomething();
}

// Function-scoped loop with return
function run() {
  while (true) {
    if (done) return;
    doSomething();
  }
}

// Configured exit call
while (true) {
  coroutine.yield();
  doSomething();
}

fast-format

Enforces oxfmt code formatting. Reports INSERT, DELETE, and REPLACE operations for formatting differences.

Features

  • ✨ Has auto-fix
  • Uses an LRU cache to avoid re-formatting unchanged files

no-async-constructor

Disallows asynchronous operations inside class constructors.

Why

Constructors return immediately, so async work causes race conditions, unhandled rejections, and incomplete object states.

Detected violations

  • await expressions
  • Promise chains (.then(), .catch(), .finally())
  • Async IIFEs ((async () => {})())
  • Unhandled async method calls (this.asyncMethod())
  • Orphaned promises (const p = this.asyncMethod())

❌ Bad

class UserService {
	constructor() {
		await this.initialize(); // Direct await
		this.loadData().then((data) => (this.data = data)); // Promise chain
		(async () => {
			await this.setup();
		})(); // Async IIFE
	}

	async initialize() {
		/* ... */
	}
	async loadData() {
		/* ... */
	}
	async setup() {
		/* ... */
	}
}

✅ Good

class UserService {
	private initPromise: Promise<void>;

	constructor() {
		this.initPromise = this.initialize();
	}

	async initialize() {
		/* ... */
	}

	// Factory pattern
	static async create(): Promise<UserService> {
		const service = new UserService();
		await service.initPromise;
		return service;
	}
}

no-commented-code

Detects and reports commented-out code.

Features

  • 💡 Has suggestions
  • Groups adjacent line comments and block comments
  • Uses heuristic detection combined with parsing to minimize false positives

❌ Bad

function calculate(x: number) {
	// const result = x * 2;
	// return result;

	/* if (x > 10) {
    return x;
  } */

	return x + 1;
}

✅ Good

function calculate(x: number) {
	// TODO: Consider multiplying by 2 instead
	// Note: This is a simplified version
	return x + 1;
}

no-identity-map

Bans pointless identity .map() calls that return the parameter unchanged.

Features

  • ✨ Has auto-fix
  • Context-aware messages for Bindings vs Arrays

❌ Bad

// Bindings
const result = scaleBinding.map((value) => value);

// Arrays - pointless shallow copy
const copied = items.map((item) => item);

✅ Good

// Bindings - use directly
const result = scaleBinding;

// Arrays - use table.clone or spread
const copied = table.clone(items);
const copied2 = [...items];

// Actual transformations are fine
const doubled = items.map((x) => x * 2);

Configuration

{
  "cease-nonsense/no-identity-map": ["error", {
    "bindingPatterns": ["binding"]  // Case-insensitive patterns
  }]
}

prevent-abbreviations

Prevent abbreviations in variable and property names. Provides suggestions for replacements and can automatically fix single-replacement cases.

Configuration

{
  "cease-nonsense/prevent-abbreviations": ["error", {
    "checkFilenames": true,
    "checkProperties": false,
    "checkVariables": true,
    "replacements": {},
    "allowList": {},
    "ignore": []
  }]
}

Features

  • ✨ Has auto-fix (for single replacements)
  • Aggressive caching of word and name replacements
  • Pre-compiled regex patterns
  • Early exits for constants and allow-listed names

❌ Bad

const err = new Error();
const fn = () => {};
const dist = calculateDistance();

✅ Good

const error = new Error();
const func = () => {};
const distance = calculateDistance();

no-shorthand-names

Bans shorthand variable names in favor of descriptive full names.

Features

  • Matches shorthands within compound identifiers (e.g., plrDataplayerData)
  • Supports glob patterns (*, ?) for flexible matching
  • Supports regex patterns (/pattern/flags) for advanced matching
  • Automatically ignores import specifiers (external packages control their naming)

Default mappings

  • plrplayer (or localPlayer for Players.LocalPlayer)
  • argsparameters
  • dtdeltaTime
  • charcharacter

Configuration

{
  "cease-nonsense/no-shorthand-names": ["error", {
    "shorthands": {
      "plr": "player",
      "*Props": "*Properties"
    },
    "allowPropertyAccess": ["char", "Props"],  // Allow as property/qualified name
    "ignoreShorthands": ["PropsWithoutRef"]    // Ignore completely
  }]
}

Options

  • shorthands: Map of shorthand patterns to replacements (exact, glob */?, or regex /pattern/flags)
  • allowPropertyAccess: Words allowed in property access (obj.prop) or type qualified names (React.Props)
  • ignoreShorthands: Words to ignore completely, regardless of context (supports same pattern syntax)

Pattern syntax

Glob patterns use * (any characters) and ? (single character):

{
  "shorthands": {
    "str*": "string*",         // strValue → stringValue
    "*Props": "*Properties",   // DataProps → DataProperties
    "*Btn*": "*Button*"        // myBtnClick → myButtonClick
  }
}

Regex patterns use /pattern/flags syntax:

{
  "shorthands": {
    "/^str(.*)$/": "string$1",  // strName → stringName
    "/^props$/i": "properties"  // Props or props → properties
  }
}

Compound identifier matching

Identifiers are split at camelCase/PascalCase boundaries, and each word is checked independently:

  • propsData with { "props": "properties" }propertiesData
  • UnitBoxBadgeInfoProps with { "Props": "Properties" }UnitBoxBadgeInfoProperties

❌ Bad

const plr = getPlayer();
const args = [1, 2, 3];
const dt = 0.016;

✅ Good

const player = getPlayer();
const localPlayer = Players.LocalPlayer;
const parameters = [1, 2, 3];
const deltaTime = 0.016;
const model = entity.char; // Property access is allowed

prefer-pattern-replacements

Enforces replacement of verbose constructor/method patterns with simpler alternatives.

Features

  • ✨ Has auto-fix
  • Type-safe pattern() API with compile-time capture validation
  • Supports captures ($x), optional args (0?), wildcards (_)
  • Constant expression evaluation (1 - 1 matches 0)
  • Same-variable matching ($x, $x requires identical arguments)
  • Scope-aware: skips fix if replacement would shadow local variable

Configuration

import { pattern } from "@pobammer-ts/eslint-cease-nonsense-rules";

{
  "cease-nonsense/prefer-pattern-replacements": ["error", {
    "patterns": [
      // Simple patterns
      pattern({
        match: "UDim2.fromScale(1, 1)",
        replacement: "oneScale"
      }),
      pattern({
        match: "UDim2.fromScale($x, $x)",
        replacement: "scale($x)"
      }),

      // Captures and conditions
      pattern({
        match: "new Vector2($x, $x)",
        replacement: "fromUniform($x)",
        when: { x: "!= 0" }
      }),

      // Optional args (0? matches 0 or missing)
      pattern({
        match: "new Vector2($x, 0?)",
        replacement: "fromX($x)"
      }),

      // Wildcards (match any value, don't capture)
      pattern({
        match: "new UDim2(_, 0, _, 0)",
        replacement: "UDim2.fromScale"
      })
    ]
  }]
}

Pattern syntax

  • $name - Capture variable, stores value for replacement
  • 0? - Optional: matches literal 0 or missing argument
  • _ - Wildcard: matches any value, not captured
  • when clause - Conditions on captures (== 0, != 0, > 5, etc.)

Ordering tip

Put the most specific patterns first. For example, keep exact shorthands like UDim2.fromScale(1, 1) before the general fallback UDim2.fromScale($x, $x) so the specific ones win.

Replacement types

  • Identifier: oneScale
  • Static access: Vector2.one
  • Call: fromUniform($x) or Vector2.fromUniform($x, $y)

❌ Bad

const scale = UDim2.fromScale(1, 1);
const vec = new Vector2(5, 5);
const offset = new Vector2(10, 0);

✅ Good

const scale = oneScale;
const vec = fromUniform(5);
const offset = fromX(10);

Scope awareness

The rule automatically skips fixes when the replacement would conflict with a local variable:

function example() {
	const oneScale = 5; // Local variable shadows replacement
	const scale = UDim2.fromScale(1, 1); // No fix applied (would shadow)
}

prefer-class-properties

Prefer class properties over assignment of literals in constructors.

Options: ['always' | 'never'] (default: 'always')

❌ Bad

class Foo {
	constructor() {
		this.bar = "literal"; // Assignment in constructor
		this.obj = { key: "value" };
	}
}

✅ Good

class Foo {
	bar = "literal"; // Class property
	obj = { key: "value" };
}

prefer-early-return

Prefer early returns over full-body conditional wrapping in function declarations.

Options: { maximumStatements: number } (default: 1)

❌ Bad

function foo() {
	if (condition) {
		doA();
		doB();
		doC();
	}
}

✅ Good

function foo() {
	if (!condition) return;
	doA();
	doB();
	doC();
}

prefer-module-scope-constants

SCREAMING_SNAKE_CASE variables must be const at module scope.

❌ Bad

let FOO = 1; // Not const
function bar() {
	const BAZ = 2; // Not module scope
}

✅ Good

const FOO = 1; // Const at module scope

// Destructuring patterns are allowed anywhere
function bar() {
	const { FOO } = config;
}

Performance

no-color3-constructor

Bans new Color3(...) except for new Color3() or new Color3(0, 0, 0). Use Color3.fromRGB() instead.

Features

  • ✨ Has auto-fix

❌ Bad

const red = new Color3(255, 0, 0);
const blue = new Color3(0.5, 0.5, 1);

✅ Good

const red = Color3.fromRGB(255, 0, 0);
const blue = Color3.fromRGB(127, 127, 255);
const black = new Color3(0, 0, 0); // Allowed

no-array-size-assignment

Disallow Roblox-style append assignment with .size() indexing. Prefer explicit append operations.

Configuration

{
  "cease-nonsense/no-array-size-assignment": ["error", {
    "allowAutofix": false // When true, rewrites safe expression statements to .push(...)
  }]
}

❌ Bad

inventory[inventory.size()] = item;
state.items[state.items.size()] = next;

✅ Good

inventory.push(item);
state.items.push(next);

no-table-create-map

Disallow mapping directly on table.create(...) and new Array(...) constructor chains.

Configuration

{
  "cease-nonsense/no-table-create-map": "error"
}

❌ Bad

const rewards = table.create(entries, ItemId.Bp1Crate).map(() => buildReward());
const values = new Array<number>(entries, 0).map((value) => value + 1);

✅ Good

const rewards = new Array<RewardData<RewardType.Item>>(entries);
for (const index of $range(1, entries)) {
	rewards[index - 1] = buildReward();
}

prefer-udim2-shorthand

Prefer UDim2.fromScale() or UDim2.fromOffset() when all offsets or all scales are zero.

Features

  • ✨ Has auto-fix
  • Leaves new UDim2(0, 0, 0, 0) alone

❌ Bad

new UDim2(1, 0, 1, 0);
new UDim2(0, 100, 0, 50);

✅ Good

UDim2.fromScale(1, 1);
UDim2.fromOffset(100, 50);
new UDim2(0, 0, 0, 0); // Allowed

prefer-sequence-overloads

Prefer the optimized ColorSequence and NumberSequence constructor overloads instead of building an array of *SequenceKeypoints.

Features

  • ✨ Has auto-fix
  • Automatically collapses identical 0/1 endpoints

❌ Bad

new ColorSequence([
	new ColorSequenceKeypoint(0, Color3.fromRGB(100, 200, 255)),
	new ColorSequenceKeypoint(1, Color3.fromRGB(255, 100, 200)),
]);

new NumberSequence([new NumberSequenceKeypoint(0, 0), new NumberSequenceKeypoint(1, 100)]);

✅ Good

new ColorSequence(Color3.fromRGB(100, 200, 255), Color3.fromRGB(255, 100, 200));

new ColorSequence(Color3.fromRGB(255, 255, 255));

new NumberSequence(0, 100);

new NumberSequence(42);

no-instance-methods-without-this

Detects instance methods that don't use this and should be converted to standalone functions.

Why

In roblox-ts, instance methods create metatable objects with significant performance overhead. Methods that don't use this can be moved outside the class.

Configuration

{
  "cease-nonsense/no-instance-methods-without-this": ["error", {
    "checkPrivate": true,   // Default: true
    "checkProtected": true, // Default: true
    "checkPublic": true     // Default: true
  }]
}

❌ Bad

type OnChange = (currentValue: number, previousValue: number) => void;

class MyClass {
	private readonly onChanges = new Array<OnChange>();
	private value = 0;

	public increment(): void {
		const previousValue = this.value;
		const value = previousValue + 1;
		this.value = value;
		this.notifyChanges(value, previousValue); // Doesn't use this
	}

	private notifyChanges(value: number, previousValue: number): void {
		for (const onChange of this.onChanges) {
			onChange(value, previousValue);
		}
	}
}

✅ Good

type OnChange = (currentValue: number, previousValue: number) => void;

function notifyChanges(value: number, previousValue: number, onChanges: ReadonlyArray<OnChange>): void {
	for (const onChange of onChanges) {
		onChange(value, previousValue);
	}
}

class MyClass {
	private readonly onChanges = new Array<OnChange>();
	private value = 0;

	public increment(): void {
		const previousValue = this.value;
		const value = previousValue + 1;
		this.value = value;
		notifyChanges(value, previousValue, this.onChanges);
	}
}

require-module-level-instantiation

Require certain classes to be instantiated at module level rather than inside functions.

Classes like Log should be instantiated once at module scope, not recreated on every function call.

Configuration

{
  "cease-nonsense/require-module-level-instantiation": ["error", {
    "classes": {
      "Log": "@rbxts/rbxts-sleitnick-log",
      "Server": "@rbxts/net"
    }
  }]
}

❌ Bad

import Log from "@rbxts/rbxts-sleitnick-log";

function useStoryModesState() {
	const log = new Log(); // Recreated on every call!
	log.Info("Create Match clicked");
}

✅ Good

import Log from "@rbxts/rbxts-sleitnick-log";

const log = new Log(); // Module level - created once

function useStoryModesState() {
	log.Info("Create Match clicked");
}

prefer-single-world-query

Enforces combining multiple world.get() or world.has() calls on the same entity into a single call for better Jecs performance.

Why

In Jecs (a Roblox ECS library), calling world.get() or world.has() multiple times on the same entity results in multiple archetype lookups. Combining these calls improves performance by reducing lookups and cache misses.

Features

  • ✨ Has auto-fix
  • Detects consecutive world.get() calls on same world and entity
  • Detects world.has() calls when ANDed together
  • Merges up to 4 components per call (Jecs limit)

❌ Bad

const position = world.get(entity, Position);
const velocity = world.get(entity, Velocity);
const health = world.get(entity, Health);

✅ Good

const [position, velocity, health] = world.get(entity, Position, Velocity, Health);

Module Boundaries

strict-component-boundaries

Prevent reaching into sibling component folders for nested modules.

Options: { allow: string[], maxDepth: number } (default: maxDepth: 1)

❌ Bad

// Reaching into another component's internals
import { helper } from "../OtherComponent/utils/helper";
import { thing } from "./components/Foo/internal";

✅ Good

// Import from shared location
import { helper } from "../../shared/helper";

// Index import from component
import { OtherComponent } from "../OtherComponent";

// Direct component import (within maxDepth)
import { Foo } from "./components/Foo";

Configuration

{
  "cease-nonsense/strict-component-boundaries": ["error", {
    "allow": ["components/\\w+$"],  // Regex patterns to allow
    "maxDepth": 2                    // Maximum import depth
  }]
}

TypeScript

array-type-generic

Disallow bracket array type syntax (T[], readonly T[]) and require generic array types.

Configuration

{
  "cease-nonsense/array-type-generic": "error"
}

❌ Bad

type Names = string[];
type Values = readonly number[];
type Pairs = [number, string][];
type Grid = string[][];

✅ Good

type Names = Array<string>;
type Values = ReadonlyArray<number>;
type Pairs = Array<[number, string]>;
type Grid = Array<Array<string>>;

dot-notation

Enforce dot notation whenever possible. In roblox-ts, it can also leave bracket notation alone when switching to dot notation would fail because the member is inaccessible at that access site.

Options: { allowKeywords?: boolean, allowPattern?: string, allowPrivateClassPropertyAccess?: boolean, allowProtectedClassPropertyAccess?: boolean, allowIndexSignaturePropertyAccess?: boolean, environment?: "standard" | "roblox-ts", allowInaccessibleClassPropertyAccess?: boolean }

❌ Bad

class Counter {
	public value = 0;
}
const counter = new Counter();
counter["value"] += 1;

✅ Good

const counter = new Counter();
counter.value += 1;

class SecretCounter {
	private value = 0;
}
function incrementValue(object: SecretCounter): void {
	object["value"] += 1;
}

Use the second form only when you intentionally opt into roblox-ts mode with allowInaccessibleClassPropertyAccess: true.

no-empty-array-literal

Disallow empty array literals ([]). Use new Array<T>() (or new Array() where contextual typing is accepted).

Configuration

{
  "cease-nonsense/no-empty-array-literal": ["error", {
    "inferTypeForEmptyArrayFix": false,
    "requireExplicitGenericOnNewArray": true,
    "ignoreInferredNonEmptyLiterals": true,
    "allowedEmptyArrayContexts": {
      "assignmentExpressions": true,
      "assignmentPatterns": true,
      "arrowFunctionBody": true,
      "callArguments": true,
      "forOfStatements": true,
      "logicalExpressions": true,
      "conditionalExpressions": true,
      "typeAssertions": true,
      "propertyValues": true,
      "returnStatements": true,
      "jsxAttributes": true
    }
  }]
}

❌ Bad

const values: Array<string> = [];
function build(input: Array<number> = []) {
  return input;
}

✅ Good

const values: Array<string> = new Array<string>();
function build(input: Array<number> = new Array<number>()) {
  return input;
}
const seeded = [1, 2, 3];

no-array-constructor-elements

Disallow element-style new Array(...) constructors and enforce environment-aware length constructor behavior.

Configuration

{
  "cease-nonsense/no-array-constructor-elements": ["error", {
    "environment": "roblox-ts",
    "requireExplicitGenericOnNewArray": true
  }]
}

❌ Bad

const letters = new Array("a", "b");
const first = new Array("a");
const list = new Array();

✅ Good

const letters = ["a", "b"];
const first = ["a"];
const list = new Array<string>();
const sized = new Array(10); // allowed in roblox-ts mode

naming-convention

Enforce naming conventions for TypeScript constructs. Optimized for common use cases like interface prefix checking without requiring type checking.

Configuration

{
  "cease-nonsense/naming-convention": ["error", {
    "custom": {
      "match": false,
      "regex": "^I[A-Z]"
    },
    "format": ["PascalCase"],
    "selector": "interface"
  }]
}

Features

  • No type checking required (fast AST-only analysis)
  • Pre-compiled regex patterns
  • Focused on common use cases

❌ Bad

// With custom: { match: false, regex: "^I[A-Z]" }
interface IUser {
	name: string;
}

✅ Good

interface User {
	name: string;
}

no-events-in-events-callback

Disallow sending Events back to the same player inside an Events.connect() callback. For Flamework networking, this prevents the anti-pattern of using Events for request/response when Functions should be used instead.

Configuration

{
  "cease-nonsense/no-events-in-events-callback": ["error", {
    "eventsImportPaths": ["shared/networking", "server/events"]
  }]
}

❌ Bad

import { Events } from "server/networking";

// Using Events for request/response
Events.units.unequipUnit.connect((player, unitKey) => {
  if (unitKey.size() > 0) {
    Events.promptNotification.fire(player, "error");
  }
});

✅ Good

import { Events, Functions } from "server/networking";

// Use Functions for request/response
Events.units.unequipUnit.connect((player, unitKey) => {
  if (unitKey.size() > 0) {
    Functions.units.notifyPlayer(player, "error");
  }
});

misleading-lua-tuple-checks

Disallow the use of LuaTuple types directly in conditional expressions, which can be misleading. Requires explicit indexing ([0]) or array destructuring.

Features

  • ✨ Has auto-fix
  • Cached type queries per node
  • WeakMap-based caching for isLuaTuple checks
  • Cached constrained type lookups

❌ Bad

// Direct LuaTuple in conditional
if (getLuaTuple()) {
	// ...
}

// LuaTuple in variable declaration
const result = getLuaTuple();

✅ Good

// Explicit indexing
if (getLuaTuple()[0]) {
	// ...
}

// Array destructuring
const [result] = getLuaTuple();

prefer-pascal-case-enums

Enum names and members must be PascalCase.

❌ Bad

enum my_enum {
	foo_bar,
}
enum MyEnum {
	FOO_BAR,
}
enum COLORS {
	red,
}

✅ Good

enum MyEnum {
	FooBar,
}
enum Color {
	Red,
	Blue,
}

prefer-singular-enums

Enum names should be singular, not plural.

❌ Bad

enum Colors {
	Red,
	Blue,
}
enum Commands {
	Up,
	Down,
}
enum Feet {
	Left,
	Right,
} // Irregular plural

✅ Good

enum Color {
	Red,
	Blue,
}
enum Command {
	Up,
	Down,
}
enum Foot {
	Left,
	Right,
}

License

MIT License - feel free to use this code however you want.