lint: configure eslint not operator spacing

The styleguide for Puter's backend expects spaces after top-level not
operators inside conditions. This commit adds an AI-generated eslint
plugin and, since it conflicts with the existing space-unary-ops plugin
from stylistic, also adds a wrapped version of space-unary-ops that
ignores top-level not operators so that this plugin will work as
expected.
This commit is contained in:
KernelDeimos
2025-11-05 15:17:59 -05:00
committed by Eric Dubé
parent e8d9b7b35d
commit 215bc9020b
6 changed files with 164 additions and 11 deletions

View File

@@ -4,7 +4,9 @@ import tseslintPlugin from '@typescript-eslint/eslint-plugin';
import tseslintParser from '@typescript-eslint/parser';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import controlStructureSpacing from './control-structure-spacing.js';
import bangSpaceIf from './eslint/bang-space-if.js';
import controlStructureSpacing from './eslint/control-structure-spacing.js';
import spaceUnaryOpsWithException from './eslint/space-unary-ops-with-exception.js';
const rules = {
'no-unused-vars': ['error', {
@@ -44,11 +46,12 @@ const rules = {
'@stylistic/space-infix-ops': ['error'],
'no-undef': 'error',
'custom/control-structure-spacing': 'error',
'custom/bang-space-if': 'error',
'@stylistic/no-trailing-spaces': 'error',
'@stylistic/space-before-blocks': ['error', 'always'],
'prefer-template': 'error',
'@stylistic/no-mixed-spaces-and-tabs': ['error', 'smart-tabs'],
'@stylistic/space-unary-ops': ['error', { words: true, nonwords: false }],
'custom/space-unary-ops-with-exception': ['error', { words: true, nonwords: false }],
'@stylistic/no-multi-spaces': ['error', { exceptions: { 'VariableDeclarator': true } }],
};
@@ -124,7 +127,11 @@ export default defineConfig([
plugins: {
js,
'@stylistic': stylistic,
custom: { rules: { 'control-structure-spacing': controlStructureSpacing } },
custom: { rules: {
'control-structure-spacing': controlStructureSpacing,
'bang-space-if': bangSpaceIf,
'space-unary-ops-with-exception': spaceUnaryOpsWithException,
} },
},
},
{

69
eslint/bang-space-if.js Normal file
View File

@@ -0,0 +1,69 @@
// eslint-plugin-bang-space-if/index.js
'use strict';
/** @type {import('eslint').ESLint.Plugin} */
export default {
meta: {
type: 'layout',
docs: {
description:
"Require a space after a top-level '!' in an if(...) condition (e.g., `if ( ! entry )`).",
recommended: false,
},
fixable: 'whitespace',
schema: [], // no options
},
create (context) {
const source = context.getSourceCode();
// Unwrap ParenthesizedExpression layers, if any
function unwrapParens (node) {
let n = node;
// ESLint/ESTree: ParenthesizedExpression is supported by espree
while ( n && n.type === 'ParenthesizedExpression' ) {
n = n.expression;
}
return n;
}
return {
IfStatement (ifNode) {
const testRaw = ifNode.test;
if ( !testRaw ) return;
const test = unwrapParens(testRaw);
if ( !test || test.type !== 'UnaryExpression' || test.operator !== '!' ) {
return; // only top-level `!` expressions
}
// Ignore boolean-cast `!!x` cases to avoid producing `! !x`
if ( test.argument && test.argument.type === 'UnaryExpression' && test.argument.operator === '!' ) {
return;
}
// Grab operator and argument tokens
const opToken = source.getFirstToken(test); // should be '!'
const argToken = source.getTokenAfter(opToken, { includeComments: false });
if ( !opToken || !argToken ) return;
// Compute current whitespace between '!' and the argument
const between = source.text.slice(opToken.range[1], argToken.range[0]);
// We want exactly one space
if ( between === ' ' ) return;
context.report({
node: test,
loc: {
start: opToken.loc.end,
end: argToken.loc.start,
},
message: "Expected a single space after top-level '!' in if(...) condition.",
fix (fixer) {
return fixer.replaceTextRange([opToken.range[1], argToken.range[0]], ' ');
},
});
},
};
},
};;;;

View File

@@ -0,0 +1,37 @@
import ruleComposer from 'eslint-rule-composer';
// Adjust this require to match the package you use for the rule.
// For eslint-stylistic v2+ the package is "@stylistic/eslint-plugin"
import stylistic from '@stylistic/eslint-plugin';
const baseRule = stylistic.rules['space-unary-ops'];
// unwrap nested parentheses
function unwrapParens (node) {
let n = node;
while ( n && n.type === 'ParenthesizedExpression' ) n = n.expression;
return n;
}
function isTopLevelBangInIfTest (node) {
if ( !node || node.type !== 'UnaryExpression' || node.operator !== '!' ) return false;
// Walk up through ancestors manually using .parent (safe in ESLint)
let current = node;
let parent = current.parent;
// Skip ParenthesizedExpression layers
while ( parent && parent.type === 'ParenthesizedExpression' ) {
current = parent;
parent = parent.parent;
}
return parent && parent.type === 'IfStatement' && unwrapParens(parent.test) === node;
}
// Filter out ONLY the reports for top-level ! inside if(...) condition
export default ruleComposer.filterReports(baseRule, (problem, context) => {
const { node } = problem;
// If this particular report is about a top-level ! in an if(...) test,
// suppress it. Otherwise, keep the original report.
return !isTopLevelBangInIfTest(node, context);
});

51
package-lock.json generated
View File

@@ -7,7 +7,6 @@
"": {
"name": "puter.com",
"version": "2.5.1",
"hasInstallScript": true,
"license": "AGPL-3.0-only",
"workspaces": [
"src/*",
@@ -15,6 +14,7 @@
"experiments/js-parse-and-output"
],
"dependencies": {
"@anthropic-ai/sdk": "^0.68.0",
"@aws-sdk/client-secrets-manager": "^3.879.0",
"@aws-sdk/client-sns": "^3.907.0",
"@google/genai": "^1.19.0",
@@ -43,6 +43,7 @@
"clean-css": "^5.3.2",
"dotenv": "^16.4.5",
"eslint": "^9.35.0",
"eslint-rule-composer": "^0.3.0",
"express": "^4.18.2",
"globals": "^15.15.0",
"html-entities": "^2.3.3",
@@ -69,12 +70,23 @@
}
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.56.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.56.0.tgz",
"integrity": "sha512-SLCB8M8+VMg1cpCucnA1XWHGWqVSZtIWzmOdDOEu3eTFZMB+A0sGZ1ESO5MHDnqrNTXz3safMrWx9x4rMZSOqA==",
"version": "0.68.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.68.0.tgz",
"integrity": "sha512-SMYAmbbiprG8k1EjEPMTwaTqssDT7Ae+jxcR5kWXiqTlbwMR2AthXtscEVWOHkRfyAV5+y3PFYTJRNa3OJWIEw==",
"license": "MIT",
"dependencies": {
"json-schema-to-ts": "^3.1.1"
},
"bin": {
"anthropic-ai-sdk": "bin/cli"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/@aws-crypto/sha256-browser": {
@@ -1098,7 +1110,6 @@
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -10677,6 +10688,16 @@
}
}
},
"node_modules/eslint-rule-composer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz",
"integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/eslint-scope": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
@@ -13146,6 +13167,19 @@
"node": "*"
}
},
"node_modules/json-schema-to-ts": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"ts-algebra": "^2.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -17978,6 +18012,12 @@
"node": ">= 14.0.0"
}
},
"node_modules/ts-algebra": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
"license": "MIT"
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -19449,7 +19489,6 @@
"version": "2.5.1",
"license": "AGPL-3.0-only",
"dependencies": {
"@anthropic-ai/sdk": "^0.56.0",
"@aws-sdk/client-polly": "^3.622.0",
"@aws-sdk/client-textract": "^3.621.0",
"@google/generative-ai": "^0.21.0",

View File

@@ -20,6 +20,7 @@
"clean-css": "^5.3.2",
"dotenv": "^16.4.5",
"eslint": "^9.35.0",
"eslint-rule-composer": "^0.3.0",
"express": "^4.18.2",
"globals": "^15.15.0",
"html-entities": "^2.3.3",
@@ -62,10 +63,10 @@
]
},
"dependencies": {
"@anthropic-ai/sdk": "^0.68.0",
"@aws-sdk/client-secrets-manager": "^3.879.0",
"@aws-sdk/client-sns": "^3.907.0",
"@google/genai": "^1.19.0",
"@anthropic-ai/sdk": "^0.68.0",
"@heyputer/putility": "^1.0.2",
"@paralleldrive/cuid2": "^2.2.2",
"@stylistic/eslint-plugin-js": "^4.4.1",
@@ -89,4 +90,4 @@
"engines": {
"node": ">=20.19.5"
}
}
}