From 215bc9020b30d2d4ebffb6fc14df8568721caf2f Mon Sep 17 00:00:00 2001 From: KernelDeimos <7225168+KernelDeimos@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:17:59 -0500 Subject: [PATCH] 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. --- eslint.config.js | 13 +++- eslint/bang-space-if.js | 69 +++++++++++++++++++ .../control-structure-spacing.js | 0 eslint/space-unary-ops-with-exception.js | 37 ++++++++++ package-lock.json | 51 ++++++++++++-- package.json | 5 +- 6 files changed, 164 insertions(+), 11 deletions(-) create mode 100644 eslint/bang-space-if.js rename control-structure-spacing.js => eslint/control-structure-spacing.js (100%) create mode 100644 eslint/space-unary-ops-with-exception.js diff --git a/eslint.config.js b/eslint.config.js index 4874064b..81197e48 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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, + } }, }, }, { diff --git a/eslint/bang-space-if.js b/eslint/bang-space-if.js new file mode 100644 index 00000000..0f62166b --- /dev/null +++ b/eslint/bang-space-if.js @@ -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]], ' '); + }, + }); + }, + }; + }, +};;;; diff --git a/control-structure-spacing.js b/eslint/control-structure-spacing.js similarity index 100% rename from control-structure-spacing.js rename to eslint/control-structure-spacing.js diff --git a/eslint/space-unary-ops-with-exception.js b/eslint/space-unary-ops-with-exception.js new file mode 100644 index 00000000..83a48d36 --- /dev/null +++ b/eslint/space-unary-ops-with-exception.js @@ -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); +}); diff --git a/package-lock.json b/package-lock.json index a6b10813..513c81ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index aed5db6c..66d875ec 100644 --- a/package.json +++ b/package.json @@ -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" } -} \ No newline at end of file +}