Skip to content

Instantly share code, notes, and snippets.

@reececomo
Last active April 10, 2025 08:22
Show Gist options
  • Select an option

  • Save reececomo/f75418edbb009d0339c79a90bbb4f110 to your computer and use it in GitHub Desktop.

Select an option

Save reececomo/f75418edbb009d0339c79a90bbb4f110 to your computer and use it in GitHub Desktop.
ESLint rule for consistent bit masks (as inlineable const enums)
/**
bitmask-enums.js
Custom rule for eslint-plugin-rulesdir
@see https://www.npmjs.com/package/eslint-plugin-rulesdir
Treats any enum with "mask" or "flag" in the name as a bit mask.
Converts to an inlineable const enum.
@example
export enum ViewerFlag {
Liked,
Subscribed,
Viewed,
Blocked,
All,
AnyInteracted = ViewerFlag.LIKED | ViewerFlag.SUBSCRIBED,
}
// auto-fixes to:
export const enum ViewerFlag {
EMPTY = 0,
LIKED = 1 << 0,
SUBSCRIBED = 1 << 1,
VIEWED = 1 << 2,
BLOCKED = 1 << 3,
ALL = 0x7FFFFFFF,
ANY_INTERACTED = ViewerFlag.LIKED | ViewerFlag.SUBSCRIBED,
}
*/
const RULE = "bitmask-enums";
const ENUM_TRIGGER_PATTERN = /(mask|flag)/i;
const MAX_INT32 = 2147483647;
const BIT_MASK_VALUE_NONE = 0x00000000;
const BIT_MASK_VALUE_ALL = 0x7FFFFFFF;
const BIT_MASK_VALUE_ALL_STRING = "0x7FFFFFFF";
const EVAL_COMPLEX_EXPRESSION = Symbol("eval_too_complex");
const BIT_MASK_BITS = [
// int8
0b00000001,
0b00000010,
0b00000100,
0b00001000,
0b00010000,
0b00100000,
0b01000000,
0b10000000,
// int16
0b00000001_00000000,
0b00000010_00000000,
0b00000100_00000000,
0b00001000_00000000,
0b00010000_00000000,
0b00100000_00000000,
0b01000000_00000000,
0b10000000_00000000,
// int24
0b00000001_00000000_00000000,
0b00000010_00000000_00000000,
0b00000100_00000000_00000000,
0b00001000_00000000_00000000,
0b00010000_00000000_00000000,
0b00100000_00000000_00000000,
0b01000000_00000000_00000000,
0b10000000_00000000_00000000,
// int32
0b00000001_00000000_00000000_00000000,
0b00000010_00000000_00000000_00000000,
0b00000100_00000000_00000000_00000000,
0b00001000_00000000_00000000_00000000,
0b00010000_00000000_00000000_00000000,
0b00100000_00000000_00000000_00000000,
0b01000000_00000000_00000000_00000000,
// 0b10000000_00000000_00000000_00000000, // ⚠️ int32 sign bit
]
const SAFE_BIT_MASK_VALUES = [
BIT_MASK_VALUE_NONE, // NONE
...BIT_MASK_BITS,
BIT_MASK_VALUE_ALL, // ALL (signed int32)
];
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: `require numeric const enums to be bit masks (${RULE})`,
url: "https://www.google.com/search?q=bit+masks",
},
fixable: "code",
schema: [],
},
create(context) {
return {
['TSEnumDeclaration']: function (node)
{
if ( !node.id.name.match( ENUM_TRIGGER_PATTERN ) )
{
return; // apply only to masks
}
if ( !node.const )
{
context.report({
node,
message: `Bit mask enums must be const enums.`,
fix(fixer) {
return fixer.insertTextBeforeRange( node.range, `const ` );
}
});
}
const assignableBitsSet = new Set( BIT_MASK_BITS );
let previousValue = 0;
// ----------------------------------------
// Check existing enum values
// ----------------------------------------
//
node.members.forEach( member => {
const value = getEnumMemberValue( member, context );
if ( typeof value === "string" )
{
return; // ✅ skip string enum values
}
// auto-fix: SNAKE_CASE members
if ( member.id.name.toUpperCase() !== member.id.name )
{
const upperSnakeName = member.id.name.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase();
context.report({
node: member,
message: `Bit mask enum member "${member.id.name}" should be named "${upperSnakeName}".`,
fix(fixer) {
return fixer.replaceText( member.id, upperSnakeName );
},
});
return;
}
if ( value === undefined )
{
// skip enum members with no value (or that rely on complex expressions)
return;
}
if ( member.id.name === "EMPTY" )
{
if ( value !== 0 )
{
context.report({
node: member,
message: `Bit mask enum member "EMPTY" must equal 0.`,
});
return;
}
}
if ( member.id.name === "ALL" )
{
if ( value !== BIT_MASK_VALUE_ALL )
{
context.report({
node: member,
message: `Bit mask enum member "ALL" must equal ${BIT_MASK_VALUE_ALL_STRING} (${BIT_MASK_VALUE_ALL}).`,
fix(fixer) {
if ( member.initializer ) {
return fixer.replaceText( member.initializer, BIT_MASK_VALUE_ALL_STRING );
}
else
{
const insertPos = member.range[0];
return fixer.insertTextAfterRange([insertPos, insertPos], ` = ${BIT_MASK_VALUE_ALL_STRING}`);
}
},
});
return;
}
}
if ( value instanceof EvalError )
{
// report eval failure
context.report({
node: member,
message: "Failed to parse / evaluate bit mask expression. Error: " + value,
});
return;
}
if ( value === EVAL_COMPLEX_EXPRESSION )
{
return; // ✅ ignore complex expressions
}
if ( typeof value !== "number" )
{
context.report({
node: member,
message: `Bit mask enum member "${member.id.name}" did not evaluate to a number: "${value}".`,
});
return;
}
if ( value > MAX_INT32 )
{
context.report({
node: member,
message: `Unsafe bit mask enum member value. Hint: Use ${BIT_MASK_VALUE_ALL_STRING} (${BIT_MASK_VALUE_ALL}) to represent 'ALL'.`,
});
return;
}
if ( ( value | 0 ) < 0 )
{
context.report({
node: member,
message: "Unsafe bit mask enum member value (signed 32-bit integer).",
});
return;
}
if ( SAFE_BIT_MASK_VALUES.indexOf( value ) === -1 )
{
context.report({
node: member,
message: `Bit mask enum member contains illegal value "${value}" (expected "1 << 0" through "1 << 30").`,
});
return;
}
if ( value < previousValue )
{
context.report({
node: member,
message: `Bit mask enum members are not in order.`,
});
return;
}
// clear any values used already
assignableBitsSet.delete( value );
if ( value !== BIT_MASK_VALUE_ALL )
{
previousValue = value;
}
});
// ----------------------------------------
// Auto-assign missing values
// ----------------------------------------
//
const assignableBits = [ ...assignableBitsSet.values() ];
node.members.forEach( member => {
if ( member.initializer?.value !== undefined ) return;
if ( getEnumMemberValue( member, context ) !== undefined ) return;
const bits = assignableBits.shift();
if ( bits === undefined )
{
context.report({
node: member,
message: `Bit mask enum has no more available values.`,
});
return;
}
// no value: auto-assign
context.report({
node: member,
message: `Bit mask enum member "${member.id.name}" is missing a value.`,
fix(fixer) {
const exponent = 31 - Math.clz32( bits );
return fixer.insertTextAfter( member.id, ` = 1 << ${exponent}` );
}
});
return;
});
// ----------------------------------------
// Insert EMPTY value
// ----------------------------------------
//
if ( getEnumMemberValue( node.members[0], context ) !== 0 )
{
context.report({
node: node,
message: "Bit mask enum first value MUST be an EMPTY value (0).",
fix(fixer) {
const sourceCode = context.getSourceCode();
const openingBrace = sourceCode.getFirstToken(node, token => token.value === '{');
const braceIndex = openingBrace.range[0];
const insertPosition = braceIndex + 1;
return fixer.insertTextBeforeRange([insertPosition, insertPosition], '\n EMPTY = 0,\n');
},
});
return;
}
// ----------------------------------------
// Auto-align values
// ----------------------------------------
//
const autoAlignMembers = node.members.filter( member => {
return true
&& member.loc.start.line === member.loc.end.line
&& member.initializer
;
});
const minimumIndent = Math.max(
...autoAlignMembers.map( member => member.id.name.length + 3 ) // include " = "
);
const indentLevel = 2 * Math.ceil( minimumIndent / 2 );
autoAlignMembers.forEach( member => {
const idx_endOfMemberName = member.id.range[1];
const idx_startOfInitializer = member.initializer.range[0];
const currentPaddingLength = idx_startOfInitializer - idx_endOfMemberName;
const correctPaddingLength = indentLevel - member.id.name.length;
if ( correctPaddingLength !== currentPaddingLength )
{
context.report({
node: member,
message: 'Bit mask enum member is not correctly aligned.',
fix(fixer) {
const padding = ' '.repeat( Math.max( 0, correctPaddingLength - 3 ) ) + " = ";
return fixer.replaceTextRange(
[idx_endOfMemberName, idx_startOfInitializer],
padding
);
},
});
return;
}
});
},
}
},
};
/**
* @returns { string | number | undefined }
*/
function getEnumMemberValue( member, context )
{
if ( !member?.initializer )
{
return undefined;
}
let value = member.initializer.value;
if ( value !== undefined )
{
return value;
}
// evaluate expressions (1 << 30)
try
{
const expression = context.getSourceCode().getText( member.initializer );
value = eval( expression );
}
catch ( error )
{
if ( ( "" + error ).includes( "is not defined" ) )
{
// some complex expression (could be self-referential, or using some constant value)
return EVAL_COMPLEX_EXPRESSION;
}
return error;
}
return value;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment