Switch from eslint to oxlint

This commit is contained in:
Jamie
2026-03-27 13:40:46 -07:00
committed by GitHub
parent 224bb811e1
commit caa10d02c3
606 changed files with 6026 additions and 3790 deletions
-42
View File
@@ -1,42 +0,0 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
const rule = require('./enforce-tw');
const RuleTester = require('eslint').RuleTester;
const message = 'Tailwind classes must be wrapped with tw()';
// avoid triggering mocha's global leak detection
require('@typescript-eslint/parser');
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
});
ruleTester.run('enforce-tw', rule, {
valid: [
{ code: `classNames("foo")` },
{ code: `<div className="foo"/>` },
{ code: `tw("flex")` },
],
invalid: [
{ code: `classNames("flex")`, errors: [{ message }] },
{ code: `<div className="flex"/>`, errors: [{ message }] },
{ code: `<div className={"flex"}/>`, errors: [{ message }] },
{ code: `classNames("foo", "flex")`, errors: [{ message }] },
{ code: `classNames(cond ? "foo" : "flex")`, errors: [{ message }] },
{ code: `classNames(cond ? "flex" : "foo")`, errors: [{ message }] },
{ code: `classNames(cond && "flex")`, errors: [{ message }] },
{ code: `classNames(cond || "flex")`, errors: [{ message }] },
{ code: `classNames(cond ?? "flex")`, errors: [{ message }] },
{ code: `classNames("foo" + "flex")`, errors: [{ message }] },
{ code: `classNames("flex" + "foo")`, errors: [{ message }] },
],
});
-134
View File
@@ -1,134 +0,0 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
const rule = require('./file-suffix.js');
const RuleTester = require('eslint').RuleTester;
// avoid triggering mocha's global leak detection
require('@typescript-eslint/parser');
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
});
ruleTester.run('file-suffix', rule, {
valid: [
// Allowed references
...[
['std', '', ['std']],
['dom', 'window.addEventListener();', ['std', 'dom']],
['node', 'require("node:fs");', ['std', 'node']],
[
'preload',
'import { ipcRenderer } from "electron";',
['std', 'node', 'preload'],
],
[
'main',
'import { autoUpdater } from "electron";',
['std', 'node', 'main'],
],
]
.map(([fileSuffix, requiredLine, depSuffixes]) => {
return depSuffixes.map(depSuffix => {
return {
name: `importing ${depSuffix} from ${fileSuffix}`,
filename: `a.${fileSuffix}.ts`,
code: `
import { x } from './b.${depSuffix}.js';
${requiredLine}
`,
globals: {
window: 'writable',
require: 'readable',
},
};
});
})
.flat(),
{
name: 'type import should have no effect',
filename: 'a.std.ts',
code: `import type { ReadonlyDeep } from './b.dom.js'`,
},
],
invalid: [
// Disallowed references
...[
['std', ['dom', 'node', 'preload', 'main']],
['dom', ['node', 'preload', 'main']],
['node', ['preload', 'main']],
['preload', ['main']],
['main', ['dom', 'preload']],
]
.map(([fileSuffix, depSuffixes]) => {
return depSuffixes.map(depSuffix => {
return {
name: `importing ${depSuffix} from ${fileSuffix}`,
filename: `a.${fileSuffix}.ts`,
code: `import { x } from './b.${depSuffix}.js'`,
errors: [
{
message: `Invalid suffix ${fileSuffix}, expected: ${depSuffix}`,
type: 'Program',
},
],
};
});
})
.flat(),
...['dom', 'node', 'preload', 'main'].map(suffix => {
return {
name: `no ${suffix} imports`,
filename: `a.${suffix}.ts`,
code: '',
errors: [
{
message: `Invalid suffix ${suffix}, expected: std`,
type: 'Program',
},
],
};
}),
// Invalid imports
{
name: 'preload in main',
filename: 'a.main.ts',
code: `
import { autoUpdater } from 'electron';
import './b.preload.js';
`,
errors: [
{
message: 'Invalid import/reference for suffix: main',
type: 'ImportDeclaration',
},
],
},
{
name: 'main in preload',
filename: 'a.preload.ts',
code: `
import { ipcRenderer } from 'electron';
import './b.main.js';
`,
errors: [
{
message: 'Invalid suffix preload, expected: main',
type: 'Program',
},
{
message: 'Invalid import/reference for suffix: main',
type: 'ImportSpecifier',
},
],
},
],
});
-58
View File
@@ -1,58 +0,0 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
function isReadOnlyDeep(node, scope) {
if (node.type !== 'TSTypeReference') {
return false;
}
let reference = scope.references.find(reference => {
return reference.identifier === node.typeName;
});
let variable = reference.resolved;
if (variable == null) {
return false;
}
let defs = variable.defs;
if (defs.length !== 1) {
return false;
}
let [def] = defs;
return (
def.type === 'ImportBinding' &&
def.parent.type === 'ImportDeclaration' &&
def.parent.source.type === 'Literal' &&
def.parent.source.value === 'type-fest'
);
}
/** @type {import("eslint").Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
hasSuggestions: false,
fixable: false,
schema: [],
},
create(context) {
return {
TSTypeAliasDeclaration(node) {
let scope = context.getScope(node);
if (isReadOnlyDeep(node.typeAnnotation, scope)) {
return;
}
context.report({
node: node.id,
message:
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
});
},
};
},
};
@@ -1,79 +0,0 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
const rule = require('./type-alias-readonlydeep');
const RuleTester = require('eslint').RuleTester;
// avoid triggering mocha's global leak detection
require('@typescript-eslint/parser');
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
});
ruleTester.run('type-alias-readonlydeep', rule, {
valid: [
{
code: `import type { ReadonlyDeep } from "type-fest"; type Foo = ReadonlyDeep<{}>`,
},
{
code: `import { ReadonlyDeep } from "type-fest"; type Foo = ReadonlyDeep<{}>`,
},
],
invalid: [
{
code: `type Foo = {}`,
errors: [
{
message:
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
type: 'Identifier',
},
],
},
{
code: `type Foo = Bar<{}>`,
errors: [
{
message:
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
type: 'Identifier',
},
],
},
{
code: `type Foo = ReadonlyDeep<{}>`,
errors: [
{
message:
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
type: 'Identifier',
},
],
},
{
code: `interface ReadonlyDeep<T> {}; type Foo = ReadonlyDeep<{}>`,
errors: [
{
message:
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
type: 'Identifier',
},
],
},
{
code: `import type { ReadonlyDeep } from "foo"; type Foo = ReadonlyDeep<{}>`,
errors: [
{
message:
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
type: 'Identifier',
},
],
},
],
});
-37
View File
@@ -1,37 +0,0 @@
components/**
coverage/**
dist/**
release/**
# Github workflows
.github/**
# Generated files
js/curve/*
js/components.js
js/util_worker.js
libtextsecure/components.js
libtextsecure/test/test.js
test/test.js
ts/protobuf/compiled.std.d.ts
storybook-static/**
build/ICUMessageParams.d.ts
# Third-party files
js/Mp3LameEncoder.min.js
js/WebAudioRecorderMp3.js
js/calling-tools/**
# TypeScript generated files
build/**/*.js
app/**/*.js
ts/**/*.js
.eslintrc.js
webpack.config.ts
preload.bundle.*
preload.wrapper.*
bundles/**
# Sticker Creator has its own eslint config
sticker-creator/**
-485
View File
@@ -1,485 +0,0 @@
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// For reference: https://github.com/airbnb/javascript
const rules = {
'comma-dangle': [
'error',
{
arrays: 'always-multiline',
objects: 'always-multiline',
imports: 'always-multiline',
exports: 'always-multiline',
functions: 'never',
},
],
// No omitting braces, keep on the same line
'brace-style': ['error', '1tbs', { allowSingleLine: false }],
curly: ['error', 'all'],
// Immer support
'no-param-reassign': [
'error',
{
props: true,
ignorePropertyModificationsForRegex: ['^draft'],
ignorePropertyModificationsFor: ['acc', 'ctx', 'context'],
},
],
// Always use === and !== except when directly comparing to null
// (which only will equal null or undefined)
eqeqeq: ['error', 'always', { null: 'never' }],
// prevents us from accidentally checking in exclusive tests (`.only`):
'mocha/no-exclusive-tests': 'error',
// encourage consistent use of `async` / `await` instead of `then`
'more/no-then': 'error',
// it helps readability to put public API at top,
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'off',
// useful for unused or internal fields
'no-underscore-dangle': 'off',
// Temp: We have because TypeScript's `allowUnreachableCode` option is on.
'no-unreachable': 'error',
// though we have a logger, we still remap console to log to disk
'no-console': 'error',
// consistently place operators at end of line except ternaries
'operator-linebreak': [
'error',
'after',
{ overrides: { '?': 'ignore', ':': 'ignore' } },
],
quotes: [
'error',
'single',
{ avoidEscape: true, allowTemplateLiterals: false },
],
'no-continue': 'off',
'lines-between-class-members': 'off',
'class-methods-use-this': 'off',
// Prettier overrides:
'arrow-parens': 'off',
'function-paren-newline': 'off',
'max-len': [
'error',
{
// Prettier generally limits line length to 80 but sometimes goes over.
// The `max-len` plugin doesnt let us omit `code` so we set it to a
// high value as a buffer to let Prettier control the line length:
code: 999,
// We still want to limit comments as before:
comments: 90,
ignoreUrls: true,
},
],
'react/jsx-props-no-spreading': 'off',
// Updated to reflect future airbnb standard
// Allows for declaring defaultProps inside a class
'react/static-property-placement': ['error', 'static public field'],
// JIRA: DESKTOP-657
'react/sort-comp': 'off',
// We don't have control over the media we're sharing, so can't require
// captions.
'jsx-a11y/media-has-caption': 'off',
// We prefer named exports
'import/prefer-default-export': 'off',
'import/enforce-node-protocol-usage': ['error', 'always'],
'import/extensions': [
'error',
'ignorePackages',
{
checkTypeImports: true,
},
],
// Prefer functional components with default params
'react/require-default-props': 'off',
// Empty fragments are used in adapters between models and react views.
'react/jsx-no-useless-fragment': [
'error',
{
allowExpressions: true,
},
],
// Our code base has tons of arrow functions passed directly to components.
'react/jsx-no-bind': 'off',
// Does not support forwardRef
'react/no-unused-prop-types': 'off',
// Not useful for us as we have lots of complicated types.
'react/destructuring-assignment': 'off',
'react/function-component-definition': [
'error',
{
namedComponents: 'function-declaration',
unnamedComponents: 'arrow-function',
},
],
'react/display-name': 'error',
'react/jsx-pascal-case': ['error', { allowNamespace: true }],
// Allow returning values from promise executors for brevity.
'no-promise-executor-return': 'off',
// Redux ducks use this a lot
'default-param-last': 'off',
'jsx-a11y/label-has-associated-control': ['error', { assert: 'either' }],
'jsx-a11y/no-static-element-interactions': 'error',
'@typescript-eslint/no-non-null-assertion': ['error'],
'@typescript-eslint/no-empty-interface': ['error'],
'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': 'error',
'no-restricted-syntax': [
'error',
{
selector: 'TSInterfaceDeclaration',
message:
'Prefer `type`. Interfaces are mutable and less powerful, so we prefer `type` for simplicity.',
},
// Defaults
{
selector: 'ForInStatement',
message:
'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.',
},
{
selector: 'LabeledStatement',
message:
'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.',
},
{
selector: 'WithStatement',
message:
'`with` is disallowed in strict mode because it makes code impossible to predict and optimize.',
},
],
'react-hooks/exhaustive-deps': [
'error',
{
additionalHooks: '^(useSpring|useSprings)$',
},
],
'local-rules/license-comments': 'error',
};
const typescriptRules = {
...rules,
'local-rules/file-suffix': 'error',
// Override brace style to enable typescript-specific syntax
'brace-style': 'off',
'@typescript-eslint/brace-style': [
'error',
'1tbs',
{ allowSingleLine: false },
],
'@typescript-eslint/array-type': ['error', { default: 'generic' }],
'no-restricted-imports': 'off',
'@typescript-eslint/no-restricted-imports': [
'error',
{
paths: [
{
name: 'chai',
importNames: ['expect', 'should', 'Should'],
message: 'Please use assert',
allowTypeImports: true,
},
],
},
],
// Overrides recommended by typescript-eslint
// https://github.com/typescript-eslint/typescript-eslint/releases/tag/v4.0.0
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-shadow': 'error',
'@typescript-eslint/no-useless-constructor': ['error'],
'@typescript-eslint/no-misused-promises': [
'error',
{
checksVoidReturn: false,
},
],
'@typescript-eslint/no-floating-promises': 'error',
// We allow "void promise", but new call-sites should use `drop(promise)`.
'no-void': ['error', { allowAsStatement: true }],
'no-shadow': 'off',
'no-useless-constructor': 'off',
// useful for unused parameters
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
// Upgrade from a warning
'@typescript-eslint/explicit-module-boundary-types': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
// Future: Maybe switch to never and always use `satisfies`
'@typescript-eslint/consistent-type-assertions': [
'error',
{
assertionStyle: 'as',
// Future: Maybe switch to allow-as-parameter or never
objectLiteralTypeAssertions: 'allow',
},
],
// Already enforced by TypeScript
'consistent-return': 'off',
// TODO: DESKTOP-4655
'import/no-cycle': 'off',
'import/no-restricted-paths': [
'error',
{
zones: [
{
target: ['ts/util', 'ts/types'],
from: ['ts/components/**', 'ts/axo/**/*.dom.*'],
message: 'Importing components is forbidden from ts/{util,types}',
},
],
},
],
'local-rules/enforce-array-buffer': 'error',
};
const TAILWIND_REPLACEMENTS = [
// inset
{ pattern: 'left-*', fix: 'start-*' },
{ pattern: 'right-*', fix: 'end-*' },
// margin
{ pattern: 'ml-*', fix: 'ms-*' },
{ pattern: 'mr-*', fix: 'me-*' },
// padding
{ pattern: 'pl-*', fix: 'ps-*' },
{ pattern: 'pr-*', fix: 'pe-*' },
// border
{ pattern: 'border-l-*', fix: 'border-s-*' },
{ pattern: 'border-r-*', fix: 'border-e-*' },
// border-radius
{ pattern: 'rounded-l', fix: 'rounded-s' },
{ pattern: 'rounded-r', fix: 'rounded-e' },
{ pattern: 'rounded-tl', fix: 'rounded-ss' },
{ pattern: 'rounded-tr', fix: 'rounded-se' },
{ pattern: 'rounded-bl', fix: 'rounded-es' },
{ pattern: 'rounded-br', fix: 'rounded-ee' },
{ pattern: 'rounded-l-*', fix: 'rounded-s-*' },
{ pattern: 'rounded-r-*', fix: 'rounded-e-*' },
{ pattern: 'rounded-tl-*', fix: 'rounded-ss-*' },
{ pattern: 'rounded-tr-*', fix: 'rounded-se-*' },
{ pattern: 'rounded-bl-*', fix: 'rounded-es-*' },
{ pattern: 'rounded-br-*', fix: 'rounded-ee-*' },
// text-align
{ pattern: 'text-left', fix: 'text-start' },
{ pattern: 'text-right', fix: 'text-end' },
// float
{ pattern: 'float-left', fix: 'float-start' },
{ pattern: 'float-right', fix: 'float-end' },
// clear
{ pattern: 'clear-left', fix: 'clear-start' },
{ pattern: 'clear-right', fix: 'clear-end' },
];
module.exports = {
root: true,
settings: {
react: {
version: 'detect',
},
'import/core-modules': ['electron'],
},
extends: ['airbnb-base', 'prettier'],
plugins: ['mocha', 'more', 'local-rules'],
overrides: [
{
files: [
'ts/**/*.ts',
'ts/**/*.tsx',
'app/**/*.ts',
'app/**/*.tsx',
'build/intl-linter/**/*.ts',
],
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'airbnb-typescript-prettier',
],
rules: typescriptRules,
},
{
files: [
'**/*.stories.tsx',
'ts/build/**',
'ts/test-*/**',
'build/intl-linter/**/*.ts',
],
rules: {
...typescriptRules,
'import/no-extraneous-dependencies': 'off',
'react/no-array-index-key': 'off',
},
},
{
files: ['ts/state/ducks/**/*.ts'],
rules: {
'local-rules/type-alias-readonlydeep': 'error',
},
},
{
files: ['ts/**/*_test.*.{ts,tsx}'],
rules: {
'func-names': 'off',
},
},
{
files: ['ts/**/*.tsx'],
plugins: ['better-tailwindcss'],
settings: {
'better-tailwindcss': {
entryPoint: './stylesheets/tailwind-config.css',
callees: ['tw'],
attributes: [],
variables: [],
},
},
rules: {
'local-rules/enforce-tw': 'error',
// stylistic: Enforce consistent line wrapping for tailwind classes. (recommended, autofix)
'better-tailwindcss/enforce-consistent-line-wrapping': 'off',
// stylistic: Enforce a consistent order for tailwind classes. (recommended, autofix)
'better-tailwindcss/enforce-consistent-class-order': 'error',
// stylistic: Enforce consistent variable syntax. (autofix)
'better-tailwindcss/enforce-consistent-variable-syntax': 'error',
// stylistic: Enforce consistent position of the important modifier. (autofix)
'better-tailwindcss/enforce-consistent-important-position': 'error',
// stylistic: Enforce shorthand class names. (autofix)
'better-tailwindcss/enforce-shorthand-classes': 'error',
// stylistic: Remove duplicate classes. (autofix)
'better-tailwindcss/no-duplicate-classes': 'error',
// stylistic: Remove deprecated classes. (autofix)
'better-tailwindcss/no-deprecated-classes': 'off',
// stylistic: Disallow unnecessary whitespace in tailwind classes. (autofix)
'better-tailwindcss/no-unnecessary-whitespace': 'error',
// correctness: Report classes not registered with tailwindcss. (recommended)
'better-tailwindcss/no-unregistered-classes': 'error',
// correctness: Report classes that produce conflicting styles.
'better-tailwindcss/no-conflicting-classes': 'error',
// correctness: Disallow restricted classes. (autofix)
'better-tailwindcss/no-restricted-classes': [
'error',
{
restrict: [
{
pattern: '\\[#[a-fA-F0-9]{3,8}?\\]', // ex: "text-[#fff]"
message: 'No arbitrary hex values',
},
{
pattern: '\\[rgba?\\(.*\\)\\]', // ex: "text-[rgb(255,255,255)]"
message: 'No arbitrary rgb values',
},
{
pattern: '\\[hsla?\\(.*\\)\\]', // ex: "text-[hsl(255,255,255)]"
message: 'No arbitrary hsl values',
},
{
pattern: '^.*!$', // ex: "p-4!"
message: 'No !important modifiers',
},
{
pattern: '^\\*+:.*', // ex: "*:mx-0",
message: 'No child variants',
},
...TAILWIND_REPLACEMENTS.map(item => {
const pattern = item.pattern.replace('*', '(.*)');
const fix = item.fix.replace('*', '$2');
return {
message: `Use logical property ${item.fix} instead of ${item.pattern}`,
pattern: `^(.*:)?${pattern}$`,
fix: `$1${fix}`,
};
}),
],
},
],
},
},
{
files: ['ts/axo/**/*.{ts,tsx}'],
rules: {
// Rule doesn't understand TypeScript namespaces
'no-inner-declarations': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-redeclare': [
'error',
{
ignoreDeclarationMerge: true,
},
],
'@typescript-eslint/explicit-module-boundary-types': [
'error',
{
allowHigherOrderFunctions: false,
},
],
},
},
],
rules: {
...rules,
'import/no-unresolved': 'off',
'import/extensions': 'off',
},
reportUnusedDisableDirectives: true,
};
+5 -9
View File
@@ -41,14 +41,12 @@ jobs:
# path: ${{ env.SCCACHE_PATH }}
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
- name: Restore cached .eslintcache and tsconfig.tsbuildinfo
- name: Restore cached tsconfig.tsbuildinfo
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
id: cache-lint
with:
path: |
.eslintcache
tsconfig.tsbuildinfo
key: lint-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**', '.eslintrc.js', '.eslint/**', 'tsconfig.json') }}
path: tsconfig.tsbuildinfo
key: lint-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**', 'tsconfig.json') }}
- name: Install Desktop node_modules
run: pnpm install
@@ -71,13 +69,11 @@ jobs:
- run: git diff --exit-code
- name: Update cached .eslintcache and tsconfig.tsbuildinfo
- name: Update cached tsconfig.tsbuildinfo
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
if: github.ref == 'refs/heads/main'
with:
path: |
.eslintcache
tsconfig.tsbuildinfo
path: tsconfig.tsbuildinfo
key: ${{ steps.cache-lint.outputs.cache-primary-key }}
macos:
-1
View File
@@ -17,7 +17,6 @@ release/
/sql/
/start.sh
.eslintcache
.stylelintcache
tsconfig.tsbuildinfo
.smartling-source.sh
+8 -1
View File
@@ -1,9 +1,16 @@
legacy-peer-deps=true
public-hoist-pattern[]=*eslint-*
minimum-release-age=14400
minimum-release-age=14400 # 10 days
minimum-release-age-exclude[]=@signalapp/*
minimum-release-age-exclude[]=@indutny/*
minimum-release-age-exclude[]=@types/*
minimum-release-age-exclude[]=electron
minimum-release-age-exclude[]=react
minimum-release-age-exclude[]=react-dom
minimum-release-age-exclude[]=oxlint
minimum-release-age-exclude[]=oxlint-tsgolint
minimum-release-age-exclude[]=@oxlint/*
minimum-release-age-exclude[]=@oxlint-tsgolint/*
minimum-release-age-exclude[]=eslint
minimum-release-age-exclude[]=@eslint/*
minimum-release-age-exclude[]=@typescript-eslint/*
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { enforceArrayBuffer } from './rules/enforceArrayBuffer.mjs';
import { enforceFileSuffix } from './rules/enforceFileSuffix.mjs';
import { enforceLicenseComments } from './rules/enforceLicenseComments.mjs';
import { enforceTw } from './rules/enforceTw.mjs';
import { enforceTypeAliasReadonlyDeep } from './rules/enforceTypeAliasReadonlyDeep.mjs';
import { noDisabledTests } from './rules/noDisabledTests.mjs';
import { noExtraneousDependencies } from './rules/noExtraneousDependencies.mjs';
import { noFocusedTests } from './rules/noFocusedTests.mjs';
import { noForIn } from './rules/noForIn.mjs';
import { noRestrictedPaths } from './rules/noRestrictedPaths.mjs';
import { noThen } from './rules/noThen.mjs';
/** @type {import("@typescript-eslint/utils").TSESLint.Linter.Plugin} */
const plugin = {
meta: {
name: 'signal-desktop',
version: '0.0.0',
},
rules: {
'enforce-array-buffer': enforceArrayBuffer,
'enforce-file-suffix': enforceFileSuffix,
'enforce-license-comments': enforceLicenseComments,
'enforce-tw': enforceTw,
'enforce-type-alias-readonlydeep': enforceTypeAliasReadonlyDeep,
'no-disabled-tests': noDisabledTests,
'no-extraneous-dependencies': noExtraneousDependencies,
'no-focused-tests': noFocusedTests,
'no-for-in': noForIn,
'no-restricted-paths': noRestrictedPaths,
'no-then': noThen,
},
};
export default plugin;
@@ -1,12 +1,18 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
/** @type {import("eslint").Rule.RuleModule} */
module.exports = {
export const enforceArrayBuffer = ESLintUtils.RuleCreator.withoutDocs({
name: 'enforce-array-buffer',
meta: {
type: 'problem',
hasSuggestions: true,
fixable: true,
fixable: 'code',
messages: {
shouldUseArrayBuffer: `Should be {{replacement}}`,
},
schema: [],
defaultOptions: [],
},
create(context) {
return {
@@ -24,13 +30,14 @@ module.exports = {
return;
}
if (node.typeParameters != null) {
if (node.typeArguments != null) {
return;
}
context.report({
node,
message: `Should be ${replacement}`,
messageId: 'shouldUseArrayBuffer',
data: { replacement },
fix(fixer) {
return [fixer.replaceTextRange(node.range, replacement)];
},
@@ -38,4 +45,4 @@ module.exports = {
},
};
},
};
});
@@ -1,31 +1,12 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { enforceArrayBuffer } from './enforceArrayBuffer.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
const rule = require('./enforce-array-buffer');
const RuleTester = require('eslint').RuleTester;
const ruleTester = new RuleTester();
// avoid triggering mocha's global leak detection
require('@typescript-eslint/parser');
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
});
const EXPECTED_ARRAY_ERROR = {
message: 'Should be Uint8Array<ArrayBuffer>',
type: 'TSTypeReference',
};
const EXPECTED_BUFFER_ERROR = {
message: 'Should be Buffer<ArrayBuffer>',
type: 'TSTypeReference',
};
ruleTester.run('enforce-array-buffer', rule, {
ruleTester.run('enforce-array-buffer', enforceArrayBuffer, {
valid: [
{ code: 'type T = number;' },
{ code: 'type T = Uint16Array;' },
@@ -52,32 +33,32 @@ ruleTester.run('enforce-array-buffer', rule, {
{
code: `type T = Uint8Array`,
output: `type T = Uint8Array<ArrayBuffer>`,
errors: [EXPECTED_ARRAY_ERROR],
errors: [{ messageId: 'shouldUseArrayBuffer' }],
},
{
code: `function f(): Uint8Array {}`,
output: `function f(): Uint8Array<ArrayBuffer> {}`,
errors: [EXPECTED_ARRAY_ERROR],
errors: [{ messageId: 'shouldUseArrayBuffer' }],
},
{
code: `function f(p: Uint8Array) {}`,
output: `function f(p: Uint8Array<ArrayBuffer>) {}`,
errors: [EXPECTED_ARRAY_ERROR],
errors: [{ messageId: 'shouldUseArrayBuffer' }],
},
{
code: `let v: Uint8Array;`,
output: `let v: Uint8Array<ArrayBuffer>;`,
errors: [EXPECTED_ARRAY_ERROR],
errors: [{ messageId: 'shouldUseArrayBuffer' }],
},
{
code: `let v: { p: Uint8Array };`,
output: `let v: { p: Uint8Array<ArrayBuffer> };`,
errors: [EXPECTED_ARRAY_ERROR],
errors: [{ messageId: 'shouldUseArrayBuffer' }],
},
{
code: `type T = Buffer`,
output: `type T = Buffer<ArrayBuffer>`,
errors: [EXPECTED_BUFFER_ERROR],
errors: [{ messageId: 'shouldUseArrayBuffer' }],
},
],
});
@@ -1,5 +1,23 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { getReferenceType } from './utils/getReferenceType.mjs';
import { isStringLiteral } from './utils/astUtils.mjs';
import { assert } from './utils/assert.mjs';
/**
* @typedef {import("@typescript-eslint/utils").TSESTree.Node} Node
* @typedef {import("@typescript-eslint/utils").TSESTree.ImportDeclaration} ImportDeclaration
* @typedef {import("@typescript-eslint/utils").TSESTree.ExportAllDeclaration} ExportAllDeclaration
* @typedef {import("@typescript-eslint/utils").TSESTree.ExportNamedDeclaration} ExportNamedDeclaration
* @typedef {import("@typescript-eslint/utils").TSESTree.ImportClause} ImportClause
* @typedef {import("@typescript-eslint/utils").TSESTree.ExportSpecifier} ExportSpecifier
*/
/**
* @typedef {'std' | 'node' | 'dom' | 'preload' | 'main'} Suffix
*/
const ELECTRON_MAIN_MODULES = new Set([
'app',
@@ -225,14 +243,7 @@ const STD_PACKAGES = new Set([
'emoji-datasource-apple',
'emoji-regex',
'eslint',
'eslint-config-airbnb-typescript-prettier',
'eslint-config-prettier',
'eslint-plugin-better-tailwindcss',
'eslint-plugin-import',
'eslint-plugin-local-rules',
'eslint-plugin-mocha',
'eslint-plugin-more',
'eslint-plugin-react',
'filesize',
'firstline',
'form-data',
@@ -279,24 +290,56 @@ const STD_PACKAGES = new Set([
'zod',
]);
/** @type {import("eslint").Rule.RuleModule} */
module.exports = {
export const enforceFileSuffix = ESLintUtils.RuleCreator.withoutDocs({
name: 'enforce-file-suffix',
meta: {
type: 'problem',
hasSuggestions: false,
fixable: false,
messages: {
missingFileSuffix: 'Missing file suffix in {{source}} import',
unrecognizedFileSuffix:
'Unrecognized file suffix in {{source}}, expected: node/preload/main/std, found: {{depSuffix}}',
commonJsImportOfElectronNoAllowed:
'CJS import of electron is not allowed',
uncategorizedElectronApi:
'Uncategorized electron API: "{{name}}". ' +
'Please update .oxlint/rules/file-suffix.js and add it to ' +
'ELECTRON_MAIN_MODULES/ELECTRON_RENDERER_MODULES/' +
'ELECTRON_SHARED_MODULES',
unsupportedNamespaceImportForElectron:
'Unsupported namespace import specifier for electron',
unsupportedImportSpecifierForElectron:
'Unsupported import specifier for electron',
uncategorizedDependency:
'Uncategorized dependency "{{moduleName}}". ' +
'Please update .oxlint/rules/file-suffix.js and add it to either ' +
'of NODE_PACKAGES/DOM_PACKAGES/STD_PACKAGES',
missingFileSuffixMustBeOneOf:
'Missing file suffix. Has to be one of: node/preload/main/std',
wrongFileSuffix:
'Invalid suffix {{fileSuffix}}, expected: {{expectedSuffix}}',
invalidImportForSuffix:
'Invalid import/reference for suffix: {{expectedSuffix}}',
invalidRequireCount: 'Invalid require() argument count',
},
schema: [],
defaultOptions: [],
},
create(context) {
const { filename, sourceCode } = context;
/** @type {string} */
let fileSuffix;
/** @type {Node[]} */
const nodeUses = [];
/** @type {Node[]} */
const domUses = [];
/** @type {Node[]} */
const preloadUses = [];
/** @type {Node[]} */
const mainUses = [];
/** @type Record<Suffix, Node[][]> */
const invalidUsesBySuffix = {
std: [nodeUses, domUses, preloadUses, mainUses],
node: [domUses, preloadUses, mainUses],
@@ -305,6 +348,10 @@ module.exports = {
main: [domUses, preloadUses],
};
/**
* @param {Node} node
* @param {string} source
*/
function trackLocalDep(node, source) {
if (!source.endsWith('.js')) {
return;
@@ -314,7 +361,8 @@ module.exports = {
if (match == null) {
context.report({
node,
message: `Missing file suffix in ${source} import`,
messageId: 'missingFileSuffix',
data: { source },
});
return;
}
@@ -333,13 +381,17 @@ module.exports = {
} else {
context.report({
node,
message:
`Unrecognized file suffix in ${source}, ` +
`expected: node/preload/main/std, found: ${depSuffix}`,
messageId: 'unrecognizedFileSuffix',
data: { source, depSuffix },
});
}
}
/**
* @param {Node} node
* @param {string} source
* @param {Array<ImportClause | ExportSpecifier> | null} specifiers
*/
function processUse(node, source, specifiers) {
if (source.startsWith('.')) {
trackLocalDep(node, source);
@@ -356,38 +408,43 @@ module.exports = {
if (source === 'electron' && specifiers == null) {
context.report({
node,
message: 'CJS import of electron is not allowed',
messageId: 'commonJsImportOfElectronNoAllowed',
});
return;
} else if (source === 'electron') {
for (const s of specifiers) {
if (s.importKind === 'type') {
continue;
}
for (const s of specifiers ?? []) {
// We implicitly skip:
// they are used in scripts
if (s.type === 'ImportSpecifier') {
if (ELECTRON_MAIN_MODULES.has(s.imported.name)) {
if (s.importKind === 'type') {
continue;
}
/** @type {string} */
let importName;
if (s.imported.type === 'Identifier') {
importName = s.imported.name;
} else {
importName = s.imported.value;
}
if (ELECTRON_MAIN_MODULES.has(importName)) {
mainUses.push(s);
} else if (ELECTRON_RENDERER_MODULES.has(s.imported.name)) {
} else if (ELECTRON_RENDERER_MODULES.has(importName)) {
preloadUses.push(s);
} else if (ELECTRON_SHARED_MODULES.has(s.imported.name)) {
} else if (ELECTRON_SHARED_MODULES.has(importName)) {
// no-op
} else {
context.report({
node: s,
message:
`Uncategorized electron API: "${s.imported.name}". ` +
'Please update .eslint/rules/file-suffix.js and add it to ' +
'ELECTRON_MAIN_MODULES/ELECTRON_RENDERER_MODULES/' +
'ELECTRON_SHARED_MODULES',
messageId: 'uncategorizedElectronApi',
data: { name: importName },
});
}
} else if (s.type === 'ImportNamespaceSpecifier') {
// import * as electron from 'electron';
context.report({
node: s,
message: 'Unsupported namespace import specifier for electron',
messageId: 'unsupportedNamespaceImportForElectron',
});
nodeUses.push(s);
} else if (s.type === 'ImportDefaultSpecifier') {
@@ -396,14 +453,20 @@ module.exports = {
} else {
context.report({
node: s,
message: 'Unsupported import specifier for electron',
messageId: 'unsupportedImportSpecifierForElectron',
});
}
}
return;
}
const [, moduleName] = source.match(/^([^@\/]+|@[^\/]+\/[^\/]+)/);
const match = source.match(/^([^@/]+|@[^/]+\/[^/]+)/);
if (match == null) {
return;
}
const [, moduleName] = match;
assert(moduleName, 'Missing moduleName');
if (NODE_PACKAGES.has(moduleName)) {
nodeUses.push(node);
} else if (source === 'react-dom/server') {
@@ -416,33 +479,50 @@ module.exports = {
} else if (!STD_PACKAGES.has(moduleName)) {
context.report({
node,
message:
`Uncategorized dependency "${moduleName}". ` +
'Please update .eslint/rules/file-suffix.js and add it to either ' +
'of NODE_PACKAGES/DOM_PACKAGES/STD_PACKAGES',
messageId: 'uncategorizedDependency',
data: { moduleName },
});
}
}
/**
* @param {ImportDeclaration | ExportAllDeclaration | ExportNamedDeclaration} node
*/
function processESMReference(node) {
if (
node.importKind === 'type' ||
(node.specifiers?.length &&
node.specifiers.every(x => x.importKind === 'type'))
) {
return;
/** @type {Array<ImportClause | ExportSpecifier> | null} */
let specifiers;
if (node.type === 'ImportDeclaration') {
if (node.importKind === 'type') {
return;
}
if (node.specifiers.length > 0) {
const allTypes = node.specifiers.every(specifier => {
return (
specifier.type === 'ImportSpecifier' &&
specifier.importKind === 'type'
);
});
if (allTypes) {
return;
}
}
specifiers = node.specifiers;
} else if (node.type === 'ExportNamedDeclaration') {
specifiers = node.specifiers;
} else {
specifiers = null;
}
if (!node.source) {
return;
}
if (node.source.type !== 'Literal') {
return;
}
const {
specifiers,
source: { value: source },
} = node;
const source = node.source.value;
processUse(node, source, specifiers);
}
@@ -457,19 +537,21 @@ module.exports = {
if (match == null) {
context.report({
node: node,
message:
'Missing file suffix. Has to be one of: node/preload/main/std',
messageId: 'missingFileSuffixMustBeOneOf',
});
return;
}
fileSuffix = match[1];
const matchedSuffix = match[1];
assert(matchedSuffix, 'Missing matchedSuffix');
fileSuffix = matchedSuffix;
},
'Program:exit': node => {
if (fileSuffix == null) {
return;
}
/** @type {Suffix} */
let expectedSuffix;
if (mainUses.length > 0) {
expectedSuffix = 'main';
@@ -500,7 +582,8 @@ module.exports = {
if (fileSuffix !== expectedSuffix) {
context.report({
node,
message: `Invalid suffix ${fileSuffix}, expected: ${expectedSuffix}`,
messageId: 'wrongFileSuffix',
data: { fileSuffix, expectedSuffix },
});
}
@@ -508,7 +591,8 @@ module.exports = {
for (const use of invalid) {
context.report({
node: use,
message: `Invalid import/reference for suffix: ${expectedSuffix}`,
messageId: 'invalidImportForSuffix',
data: { expectedSuffix },
});
}
},
@@ -529,27 +613,29 @@ module.exports = {
return;
}
const scope = sourceCode.getScope(node);
const ref = scope.references.find(r => r.identifier === node.callee);
if (ref.resolved.scope.type !== 'global') {
const refType = getReferenceType(sourceCode, node.callee);
if (refType !== 'global') {
return;
}
const { arguments: args } = node;
if (args.length !== 1) {
context.report({
node,
message: 'Invalid require() argument count',
messageId: 'invalidRequireCount',
});
return;
}
const [arg] = args;
assert(arg, 'Missing arg');
/** @type {string} */
let source;
if (arg.type === 'Literal') {
if (isStringLiteral(arg)) {
source = arg.value;
} else if (
arg.type === 'TSAsExpression' &&
arg.expression.type === 'Literal'
isStringLiteral(arg.expression)
) {
source = arg.expression.value;
} else {
@@ -557,23 +643,22 @@ module.exports = {
return;
}
processUse(node, source, undefined);
processUse(node, source, null);
},
Identifier(node) {
if (node.name !== 'window' && node.name !== 'document') {
return;
}
const scope = sourceCode.getScope(node);
const ref = scope.references.find(r => r.identifier === node);
if (ref == null) {
const refType = getReferenceType(sourceCode, node);
if (refType == null) {
// Not part of expression
return;
}
if (ref.resolved.scope.type !== 'global') {
if (refType !== 'global') {
return;
}
domUses.push(node);
},
};
},
};
});
+139
View File
@@ -0,0 +1,139 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { enforceFileSuffix } from './enforceFileSuffix.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
const ruleTester = new RuleTester();
const ALLOWED_REFERENCES = /* @type {const} */ [
{
fileSuffix: 'std',
requiredLine: '',
depSuffixes: ['std'],
},
{
fileSuffix: 'dom',
requiredLine: 'window.addEventListener();',
depSuffixes: ['std', 'dom'],
},
{
fileSuffix: 'node',
requiredLine: 'require("node:fs");',
depSuffixes: ['std', 'node'],
},
{
fileSuffix: 'preload',
requiredLine: 'import { ipcRenderer } from "electron";',
depSuffixes: ['std', 'node', 'preload'],
},
{
fileSuffix: 'main',
requiredLine: 'import { autoUpdater } from "electron";',
depSuffixes: ['std', 'node', 'main'],
},
];
const DISALLOWED_REFERENCES = /* @type {const} */ [
{ fileSuffix: 'std', depSuffixes: ['dom', 'node', 'preload', 'main'] },
{ fileSuffix: 'dom', depSuffixes: ['node', 'preload', 'main'] },
{ fileSuffix: 'node', depSuffixes: ['preload', 'main'] },
{ fileSuffix: 'preload', depSuffixes: ['main'] },
{ fileSuffix: 'main', depSuffixes: ['dom', 'preload'] },
];
ruleTester.run('file-suffix', enforceFileSuffix, {
valid: [
...ALLOWED_REFERENCES.map(({ fileSuffix, requiredLine, depSuffixes }) => {
return depSuffixes.map(depSuffix => {
/** @type {const} */
return {
name: `importing ${depSuffix} from ${fileSuffix}`,
filename: `a.${fileSuffix}.ts`,
code: `
import { x } from './b.${depSuffix}.js';
${requiredLine}
`,
languageOptions: {
globals: {
window: 'writable',
require: 'readable',
},
},
};
});
}).flat(),
{
name: 'type import should have no effect',
filename: 'a.std.ts',
code: `import type { ReadonlyDeep } from './b.dom.js'`,
},
],
invalid: [
...DISALLOWED_REFERENCES.map(({ fileSuffix, depSuffixes }) => {
return depSuffixes.map(depSuffix => {
/** @type {const} */
return {
name: `importing ${depSuffix} from ${fileSuffix}`,
filename: `a.${fileSuffix}.ts`,
code: `import { x } from './b.${depSuffix}.js'`,
errors: [
{
messageId: 'wrongFileSuffix',
data: { fileSuffix, expectedSuffix: depSuffix },
},
],
};
});
}).flat(),
...['dom', 'node', 'preload', 'main'].map(fileSuffix => {
/** @type {const} */
return {
name: `no ${fileSuffix} imports`,
filename: `a.${fileSuffix}.ts`,
code: '',
errors: [
{
messageId: 'wrongFileSuffix',
data: { fileSuffix, expectedSuffix: 'std' },
},
],
};
}),
// Invalid imports
{
name: 'preload in main',
filename: 'a.main.ts',
code: `
import { autoUpdater } from 'electron';
import './b.preload.js';
`,
errors: [
{
messageId: 'invalidImportForSuffix',
data: { expectedSuffix: 'main' },
},
],
},
{
name: 'main in preload',
filename: 'a.preload.ts',
code: `
import { ipcRenderer } from 'electron';
import './b.main.js';
`,
errors: [
{
messageId: 'wrongFileSuffix',
data: { fileSuffix: 'preload', expectedSuffix: 'main' },
},
{
messageId: 'invalidImportForSuffix',
data: { expectedSuffix: 'main' },
},
],
},
],
});
@@ -1,24 +1,29 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
const COMMENT_LINE_1_EXACT = /^ Copyright \d{4} Signal Messenger, LLC$/;
const COMMENT_LINE_2_EXACT = /^ SPDX-License-Identifier: AGPL-3.0-only$/;
const COMMENT_LINE_1_LOOSE = /Copyright (\d{4}) Signal Messenger, LLC/;
const COMMENT_LINE_2_LOOSE = /SPDX-License-Identifier: AGPL-3.0-only/;
/** @type {import("eslint").Rule.RuleModule} */
module.exports = {
export const enforceLicenseComments = ESLintUtils.RuleCreator.withoutDocs({
meta: {
type: 'problem',
hasSuggestions: false,
fixable: true,
fixable: 'code',
messages: {
missingLicenseComment: 'Missing license comment',
},
schema: [],
defaultOptions: [],
},
create(context) {
return {
Program(node) {
let comment1 = node.comments.at(0);
let comment2 = node.comments.at(1);
const comment1 = node.comments?.at(0);
const comment2 = node.comments?.at(1);
if (
comment1?.type === 'Line' &&
@@ -31,15 +36,14 @@ module.exports = {
context.report({
node,
message: 'Missing license comment',
messageId: 'missingLicenseComment',
fix(fixer) {
let year = null;
let remove = [];
const remove = [];
for (let comment of node.comments) {
let match1 = comment.value.match(COMMENT_LINE_1_LOOSE);
let match2 = comment.value.match(COMMENT_LINE_2_LOOSE);
for (const comment of node.comments ?? []) {
const match1 = comment.value.match(COMMENT_LINE_1_LOOSE);
const match2 = comment.value.match(COMMENT_LINE_2_LOOSE);
if (match1 != null) {
year = match1[1];
@@ -52,7 +56,7 @@ module.exports = {
year ??= new Date().getFullYear().toString();
let insert =
const insert =
`// Copyright ${year} Signal Messenger, LLC\n` +
'// SPDX-License-Identifier: AGPL-3.0-only\n';
@@ -70,4 +74,4 @@ module.exports = {
},
};
},
};
});
@@ -1,18 +1,30 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
const { createSyncFn } = require('synckit');
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { createSyncFn } from 'synckit';
const worker = createSyncFn(require.resolve('./enforce-tw.worker.js'));
/**
* @typedef {import("@typescript-eslint/utils").TSESTree.Node} Node
*/
/** @type {import("eslint").Rule.RuleModule} */
module.exports = {
const worker = createSyncFn(import.meta.resolve('./enforceTw.worker.mjs'));
export const enforceTw = ESLintUtils.RuleCreator.withoutDocs({
name: 'enforce-tw',
meta: {
type: 'problem',
hasSuggestions: true,
fixable: true,
messages: {
needsTw: 'Tailwind classes must be wrapped with tw()',
},
schema: [],
defaultOptions: [],
},
create(context) {
/**
* @param {string} input
* @param {Node} node
*/
function check(input, node) {
if (typeof input !== 'string') {
throw new Error(`Unexpected input ${input} for node type ${node.type}`);
@@ -35,11 +47,14 @@ module.exports = {
column: node.loc.start.column + index + length,
},
},
message: 'Tailwind classes must be wrapped with tw()',
messageId: 'needsTw',
});
}
}
/**
* @param {Node} node
*/
function traverse(node) {
if (node.type === 'Literal') {
if (typeof node.value === 'string') {
@@ -47,14 +62,16 @@ module.exports = {
}
// ignore other literals
} else if (node.type === 'TemplateLiteral') {
for (let element of node.quasis) {
for (const element of node.quasis) {
traverse(element);
}
for (let expression of node.expressions) {
for (const expression of node.expressions) {
traverse(expression);
}
} else if (node.type === 'TemplateElement') {
check(node.value.cooked, node);
if (node.value.cooked != null) {
check(node.value.cooked, node);
}
} else if (node.type === 'JSXExpressionContainer') {
traverse(node.expression);
} else if (node.type === 'ConditionalExpression') {
@@ -74,7 +91,7 @@ module.exports = {
throw new Error(`Unexpected binary operator: ${node.operator}`);
}
} else if (node.type === 'ObjectExpression') {
for (let prop of node.properties) {
for (const prop of node.properties) {
traverse(prop);
}
} else if (node.type === 'Property') {
@@ -93,8 +110,10 @@ module.exports = {
throw new Error(`Unexpected property key type: ${node.key.type}`);
}
} else if (node.type === 'ArrayExpression') {
for (let element of node.elements) {
traverse(element);
for (const element of node.elements) {
if (element != null) {
traverse(element);
}
}
} else if (node.type === 'Identifier') {
// ignore
@@ -111,15 +130,17 @@ module.exports = {
CallExpression(node) {
if (node.callee.type !== 'Identifier') return;
if (node.callee.name !== 'classNames') return;
for (let arg of node.arguments) {
for (const arg of node.arguments) {
traverse(arg);
}
},
JSXAttribute(node) {
if (node.name.type !== 'JSXIdentifier') return;
if (node.name.name !== 'className') return;
traverse(node.value);
if (node.value != null) {
traverse(node.value);
}
},
};
},
};
});
+69
View File
@@ -0,0 +1,69 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { enforceTw } from './enforceTw.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
});
ruleTester.run('enforce-tw', enforceTw, {
valid: [
{ code: `classNames("foo")` },
{ code: `<div className="foo"/>` },
{ code: `tw("flex")` },
],
invalid: [
{
code: `classNames("flex")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `<div className="flex"/>`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `<div className={"flex"}/>`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames("foo", "flex")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames(cond ? "foo" : "flex")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames(cond ? "flex" : "foo")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames(cond && "flex")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames(cond || "flex")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames(cond ?? "flex")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames("foo" + "flex")`,
errors: [{ messageId: 'needsTw' }],
},
{
code: `classNames("flex" + "foo")`,
errors: [{ messageId: 'needsTw' }],
},
],
});
@@ -1,12 +1,13 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
const { runAsWorker } = require('synckit');
const enhancedResolve = require('enhanced-resolve');
const tailwind = require('tailwindcss');
const path = require('node:path');
const fs = require('node:fs');
// @ts-check
import { runAsWorker } from 'synckit';
import enhancedResolve from 'enhanced-resolve';
import * as tailwind from 'tailwindcss';
import path from 'node:path';
import fs from 'node:fs';
const rootDir = path.join(__dirname, '../..');
const rootDir = path.join(import.meta.dirname, '../..');
const tailwindCssPath = path.join(rootDir, 'stylesheets/tailwind-config.css');
async function loadDesignSystem() {
@@ -21,12 +22,13 @@ async function loadDesignSystem() {
tailwindCss,
{
base: path.dirname(tailwindCssPath),
loadStylesheet(id, base) {
async loadStylesheet(id, base) {
const resolved = resolver(base, id);
if (!resolved) {
return { base: '', content: '' };
return { path: '', base: '', content: '' };
}
return {
path: resolved,
base: path.dirname(resolved),
content: fs.readFileSync(resolved, 'utf-8'),
};
@@ -39,12 +41,17 @@ async function loadDesignSystem() {
let cachedDesignSystem = null;
runAsWorker(async classNames => {
/**
* @param {Array<string>} classNames
*/
async function worker(classNames) {
cachedDesignSystem ??= await loadDesignSystem();
const designSystem = cachedDesignSystem;
const css = designSystem.candidatesToCss(classNames);
const tailwindClassNames = classNames.filter((_, index) => {
return css.at(index) !== null;
return css.at(index) != null;
});
return tailwindClassNames;
});
}
runAsWorker(worker);
@@ -0,0 +1,75 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { assert } from './utils/assert.mjs';
/**
* @typedef {import("@typescript-eslint/utils").TSESTree.Node} Node
* @typedef {import("@typescript-eslint/utils").TSESLint.Scope.Scope} Scope
*/
/**
* @param {Node} node
* @param {Scope} scope
*/
function isReadOnlyDeep(node, scope) {
if (node.type !== 'TSTypeReference') {
return false;
}
const reference = scope.references.find(ref => {
return ref.identifier === node.typeName;
});
const variable = reference?.resolved;
if (variable == null) {
return false;
}
const defs = variable.defs;
if (defs.length !== 1) {
return false;
}
const [def] = defs;
assert(def, 'Missing def');
return (
def.type === 'ImportBinding' &&
def.parent.type === 'ImportDeclaration' &&
def.parent.source.type === 'Literal' &&
def.parent.source.value === 'type-fest'
);
}
export const enforceTypeAliasReadonlyDeep = ESLintUtils.RuleCreator.withoutDocs(
{
name: 'enforce-type-alias-readonlydeep',
meta: {
type: 'problem',
messages: {
needsReadonlyDeep:
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
},
schema: [],
defaultOptions: [],
},
create(context) {
return {
TSTypeAliasDeclaration(node) {
const scope = context.sourceCode.getScope(node);
if (isReadOnlyDeep(node.typeAnnotation, scope)) {
return;
}
context.report({
node: node.id,
messageId: 'needsReadonlyDeep',
});
},
};
},
}
);
@@ -0,0 +1,40 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { enforceTypeAliasReadonlyDeep } from './enforceTypeAliasReadonlyDeep.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
const ruleTester = new RuleTester();
ruleTester.run('type-alias-readonlydeep', enforceTypeAliasReadonlyDeep, {
valid: [
{
code: `import type { ReadonlyDeep } from "type-fest"; type Foo = ReadonlyDeep<{}>`,
},
{
code: `import { ReadonlyDeep } from "type-fest"; type Foo = ReadonlyDeep<{}>`,
},
],
invalid: [
{
code: `type Foo = {}`,
errors: [{ messageId: 'needsReadonlyDeep' }],
},
{
code: `type Foo = Bar<{}>`,
errors: [{ messageId: 'needsReadonlyDeep' }],
},
{
code: `type Foo = ReadonlyDeep<{}>`,
errors: [{ messageId: 'needsReadonlyDeep' }],
},
{
code: `interface ReadonlyDeep<T> {}; type Foo = ReadonlyDeep<{}>`,
errors: [{ messageId: 'needsReadonlyDeep' }],
},
{
code: `import type { ReadonlyDeep } from "foo"; type Foo = ReadonlyDeep<{}>`,
errors: [{ messageId: 'needsReadonlyDeep' }],
},
],
});
@@ -0,0 +1,22 @@
{
"dependencies": {
"prod-dep": "0.0.0",
"@scoped/prod-dep": "0.0.0"
},
"devDependencies": {
"dev-dep": "0.0.0",
"@scoped/dev-dep": "0.0.0"
},
"peerDependencies": {
"peer-dep": "0.0.0",
"@scoped/peer-dep": "0.0.0"
},
"optionalDependencies": {
"optional-dep": "0.0.0",
"@scoped/optional-dep": "0.0.0"
},
"bundledDependencies": [
"bundled-dep",
"@scoped/bundled-dep"
]
}
@@ -0,0 +1,3 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export {};
@@ -0,0 +1,3 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export {};
@@ -0,0 +1,4 @@
{
"include": ["./client/**", "./server/**"],
"compilerOptions": {}
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { getReferenceType } from './utils/getReferenceType.mjs';
import { isPropertyAccess } from './utils/astUtils.mjs';
export const noDisabledTests = ESLintUtils.RuleCreator.withoutDocs({
name: 'no-disabled-tests',
meta: {
type: 'problem',
hasSuggestions: true,
messages: {
unexpectedDisabledTest: 'Unexpected disabled test',
removeSkip: 'Remove .skip()',
},
schema: [],
defaultOptions: [],
},
create(context) {
const { sourceCode } = context;
return {
MemberExpression(node) {
if (node.object.type !== 'Identifier') {
return;
}
let replacement;
if (node.object.name === 'describe') {
replacement = 'describe';
} else if (node.object.name === 'it') {
replacement = 'it';
} else if (node.object.name === 'test') {
replacement = 'test';
} else {
return;
}
if (!isPropertyAccess(node, 'skip')) {
return;
}
const refType = getReferenceType(sourceCode, node.object);
if (refType != null && refType !== 'global') {
return;
}
context.report({
node,
messageId: 'unexpectedDisabledTest',
suggest: [
{
messageId: 'removeSkip',
fix(fixer) {
return [fixer.replaceTextRange(node.range, replacement)];
},
},
],
});
},
};
},
});
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { noDisabledTests } from './noDisabledTests.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
const ruleTester = new RuleTester();
ruleTester.run('no-disabled-tests', noDisabledTests, {
valid: [
{ code: 'describe(() => {});' },
{ code: 'it(() => {});' },
{ code: 'test(() => {});' },
{ code: 'describe.only(() => {});' },
{ code: 'it.only(() => {});' },
{ code: 'test.only(() => {});' },
{ code: 'let describe; describe.skip(() => {});' },
{ code: 'x.describe.skip(() => {});' },
],
invalid: [
{
code: `describe.skip(() => {});`,
suggestion: `describe(() => {});`,
},
{
code: `it.skip(() => {});`,
suggestion: `it(() => {});`,
},
{
code: `test.skip(() => {});`,
suggestion: `test(() => {});`,
},
{
code: `describe['skip'](() => {});`,
suggestion: `describe(() => {});`,
},
{
code: `it['skip'](() => {});`,
suggestion: `it(() => {});`,
},
{
code: `test['skip'](() => {});`,
suggestion: `test(() => {});`,
},
].map(opts => {
return {
code: opts.code,
errors: [
{
messageId: 'unexpectedDisabledTest',
suggestions: [{ messageId: 'removeSkip', output: opts.suggestion }],
},
],
};
}),
});
+202
View File
@@ -0,0 +1,202 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { readFileSync } from 'node:fs';
import { dirname } from 'node:path';
import { isBuiltin, findPackageJSON } from 'node:module';
import { createImportSourceVisitor } from './utils/createImportSourceVisitor.mjs';
/**
* @param value {unknown}
* @returns {value is Record<string, unknown>}
*/
function isObject(value) {
return typeof value === 'object' && value != null;
}
/**
* @param deps {unknown}
*/
function getDepsKeys(deps) {
return new Set(isObject(deps) ? Object.keys(deps) : null);
}
/**
* @param deps {unknown}
* @returns {Set<string>}
*/
function getBundledDepsKeys(deps) {
return Array.isArray(deps) ? new Set(deps) : getDepsKeys(deps);
}
/**
* @typedef {object} PkgDeps
* @property {Set<string>} dependencies
* @property {Set<string>} devDependencies
* @property {Set<string>} peerDependencies
* @property {Set<string>} optionalDependencies
* @property {Set<string>} bundledDependencies
*/
/** @type {Map<string, PkgDeps>} */
const PKG_DEPS_CACHE = new Map();
/** @param {string} currentFile */
function getPkgDeps(currentFile) {
const currentDir = dirname(currentFile);
const cached = PKG_DEPS_CACHE.get(currentDir);
if (cached != null) {
return cached;
}
const pkgPath = findPackageJSON('.', currentFile);
if (pkgPath == null) {
throw new Error(`Could not resolve package.json from ${currentFile}`);
}
const pkgText = readFileSync(pkgPath, 'utf8');
const pkgJson = JSON.parse(pkgText);
/** @type {PkgDeps} */
const pkgDeps = {
dependencies: getDepsKeys(pkgJson.dependencies),
devDependencies: getDepsKeys(pkgJson.devDependencies),
peerDependencies: getDepsKeys(pkgJson.peerDependencies),
optionalDependencies: getDepsKeys(pkgJson.optionalDependencies),
bundledDependencies: getBundledDepsKeys(pkgJson.bundledDependencies),
};
PKG_DEPS_CACHE.set(currentDir, pkgDeps);
return pkgDeps;
}
/** @param {string} source */
function getPackageNameFromSource(source) {
if (source.startsWith('@')) {
const [scope, name] = source.split('/', 2);
return `${scope}/${name}`;
}
const [name] = source.split('/', 1);
return `${name}`;
}
/**
* @typedef {object} Options
* @property {boolean=} devDependencies
* @property {boolean=} peerDependencies
* @property {boolean=} optionalDependencies
* @property {boolean=} bundledDependencies
*/
/** @type {[Options]} */
const defaultOptions = [
{
devDependencies: true,
peerDependencies: true,
optionalDependencies: true,
bundledDependencies: true,
},
];
export const noExtraneousDependencies = ESLintUtils.RuleCreator.withoutDocs({
name: 'no-extraneous-dependencies',
meta: {
type: 'problem',
messages: {
missingFromProjectDeps:
"'{{pkgName}}' should be listed in the project's dependencies",
wrongProjectDeps:
"'{{pkgName}}' should be listed in the project's dependencies, found in {{found}}",
},
schema: [
{
type: 'object',
properties: {
devDependencies: { type: 'boolean' },
peerDependencies: { type: 'boolean' },
optionalDependencies: { type: 'boolean' },
bundledDependencies: { type: 'boolean' },
},
additionalProperties: false,
},
],
defaultOptions,
},
create(context) {
const { sourceCode, options } = context;
const opts = {
devDependencies: options[0]?.devDependencies ?? true,
peerDependencies: options[0]?.peerDependencies ?? true,
optionalDependencies: options[0]?.optionalDependencies ?? true,
bundledDependencies: options[0]?.bundledDependencies ?? true,
};
const pkgDeps = getPkgDeps(context.physicalFilename);
return createImportSourceVisitor(sourceCode, node => {
const source = node.value;
if (
source.startsWith('.') ||
source.startsWith('/') ||
source.trim() === ''
) {
return;
}
if (isBuiltin(source)) {
return;
}
const pkgName = getPackageNameFromSource(source);
/** @type {Array<string>} */
const found = [];
if (pkgDeps.dependencies.has(pkgName)) {
return;
}
if (pkgDeps.devDependencies.has(pkgName)) {
found.push('devDependencies');
if (opts.devDependencies) {
return;
}
}
if (pkgDeps.peerDependencies.has(pkgName)) {
found.push('peerDependencies');
if (opts.peerDependencies) {
return;
}
}
if (pkgDeps.optionalDependencies.has(pkgName)) {
found.push('optionalDependencies');
if (opts.optionalDependencies) {
return;
}
}
if (pkgDeps.bundledDependencies.has(pkgName)) {
found.push('bundledDependencies');
if (opts.bundledDependencies) {
return;
}
}
if (found.length > 0) {
context.report({
node,
messageId: 'wrongProjectDeps',
data: { pkgName, found: found.join(', ') },
});
} else {
context.report({
node,
messageId: 'missingFromProjectDeps',
data: { pkgName },
});
}
});
},
});
@@ -0,0 +1,112 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import path from 'node:path';
import { noExtraneousDependencies } from './noExtraneousDependencies.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
/**
* @typedef {import("./noExtraneousDependencies.mjs").Options} Options
*/
const ruleTester = new RuleTester();
const filename = path.join(
import.meta.dirname,
'fixtures/noExtraneousDependencies/package/foo.js'
);
/** @type {Options} */
const NONE = {
devDependencies: false,
peerDependencies: false,
optionalDependencies: false,
bundledDependencies: false,
};
/**
* @satisfies {Record<string, [Options]>}
*/
const opts = {
none: [NONE],
dev: [{ ...NONE, devDependencies: true }],
peer: [{ ...NONE, peerDependencies: true }],
optional: [{ ...NONE, optionalDependencies: true }],
bundled: [{ ...NONE, bundledDependencies: true }],
};
ruleTester.run('no-extraneous-dependencies', noExtraneousDependencies, {
valid: [
{ filename, code: `import a from "./a";`, options: opts.none },
{ filename, code: `import a from "../a";`, options: opts.none },
{ filename, code: `import a from "path";`, options: opts.none },
{ filename, code: `import a from "node:path";`, options: opts.none },
{ filename, code: `import a from "";`, options: opts.none },
{ filename, code: `import a from "prod-dep";`, options: opts.none },
{ filename, code: `import a from "prod-dep/nested";`, options: opts.none },
{ filename, code: `import a from "@scoped/prod-dep";`, options: opts.none },
{
filename,
code: `import a from "@scoped/prod-dep/nested";`,
options: opts.none,
},
{ filename, code: `import a from "dev-dep";`, options: opts.dev },
{ filename, code: `import a from "peer-dep";`, options: opts.peer },
{
filename,
code: `import a from "optional-dep";`,
options: opts.optional,
},
{ filename, code: `import a from "bundled-dep";`, options: opts.bundled },
],
invalid: [
{
filename,
code: `import a from "dev-dep";`,
options: opts.none,
errors: [{ messageId: 'wrongProjectDeps' }],
},
{
filename,
code: `import a from "peer-dep";`,
options: opts.none,
errors: [{ messageId: 'wrongProjectDeps' }],
},
{
filename,
code: `import a from "optional-dep";`,
options: opts.none,
errors: [{ messageId: 'wrongProjectDeps' }],
},
{
filename,
code: `import a from "bundled-dep";`,
options: opts.none,
errors: [{ messageId: 'wrongProjectDeps' }],
},
{
filename,
code: `import a from "dev-dep";`,
options: opts.peer,
errors: [{ messageId: 'wrongProjectDeps' }],
},
{
filename,
code: `import a from "dev-dep";`,
options: opts.optional,
errors: [{ messageId: 'wrongProjectDeps' }],
},
{
filename,
code: `import a from "dev-dep";`,
options: opts.bundled,
errors: [{ messageId: 'wrongProjectDeps' }],
},
{
filename,
code: `import a from "does-not-exist";`,
options: opts.bundled,
errors: [{ messageId: 'missingFromProjectDeps' }],
},
],
});
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { getReferenceType } from './utils/getReferenceType.mjs';
import { isPropertyAccess } from './utils/astUtils.mjs';
export const noFocusedTests = ESLintUtils.RuleCreator.withoutDocs({
name: 'no-focused-tests',
meta: {
type: 'problem',
hasSuggestions: true,
fixable: 'code',
messages: {
unexpectedFocusedTest: 'Unexpected focused test',
},
schema: [],
},
create(context) {
const { sourceCode } = context;
return {
MemberExpression(node) {
if (node.object.type !== 'Identifier') {
return;
}
let replacement;
if (node.object.name === 'describe') {
replacement = 'describe';
} else if (node.object.name === 'it') {
replacement = 'it';
} else if (node.object.name === 'test') {
replacement = 'test';
} else {
return;
}
if (!isPropertyAccess(node, 'only')) {
return;
}
const refType = getReferenceType(sourceCode, node.object);
if (refType != null && refType !== 'global') {
return;
}
context.report({
node,
messageId: 'unexpectedFocusedTest',
fix(fixer) {
if (node.range == null) {
return null;
}
return [fixer.replaceTextRange(node.range, replacement)];
},
});
},
};
},
});
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { noFocusedTests } from './noFocusedTests.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
const ruleTester = new RuleTester();
ruleTester.run('no-focused-tests', noFocusedTests, {
valid: [
{ code: 'describe(() => {});' },
{ code: 'it(() => {});' },
{ code: 'test(() => {});' },
{ code: 'describe.skip(() => {});' },
{ code: 'it.skip(() => {});' },
{ code: 'test.skip(() => {});' },
{ code: 'let describe; describe.only(() => {});' },
{ code: 'x.describe.only(() => {});' },
],
invalid: [
{
code: `describe.only(() => {});`,
output: `describe(() => {});`,
errors: [{ messageId: 'unexpectedFocusedTest' }],
},
{
code: `it.only(() => {});`,
output: `it(() => {});`,
errors: [{ messageId: 'unexpectedFocusedTest' }],
},
{
code: `test.only(() => {});`,
output: `test(() => {});`,
errors: [{ messageId: 'unexpectedFocusedTest' }],
},
{
code: `describe['only'](() => {});`,
output: `describe(() => {});`,
errors: [{ messageId: 'unexpectedFocusedTest' }],
},
{
code: `it['only'](() => {});`,
output: `it(() => {});`,
errors: [{ messageId: 'unexpectedFocusedTest' }],
},
{
code: `test['only'](() => {});`,
output: `test(() => {});`,
errors: [{ messageId: 'unexpectedFocusedTest' }],
},
],
});
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
export const noForIn = ESLintUtils.RuleCreator.withoutDocs({
name: 'no-for-in',
meta: {
type: 'problem',
messages: {
preferForOf: 'Prefer for..of loops',
},
schema: [],
},
create(context) {
return {
ForInStatement(node) {
context.report({
node,
messageId: 'preferForOf',
});
},
};
},
});
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { noForIn } from './noForIn.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
const ruleTester = new RuleTester();
ruleTester.run('no-for-in', noForIn, {
valid: [
{ code: 'for (let a of b) {}' },
{ code: 'for (;;) {}' },
{ code: 'if (a in b) {}' },
],
invalid: [
{
code: `for (let a in b) {}`,
errors: [{ messageId: 'preferForOf' }],
},
{
code: `for (a in b) {}`,
errors: [{ messageId: 'preferForOf' }],
},
],
});
+264
View File
@@ -0,0 +1,264 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { createImportSourceVisitor } from './utils/createImportSourceVisitor.mjs';
import micromatch from 'micromatch';
import isGlob from 'is-glob';
import * as path from 'node:path';
import { assert } from './utils/assert.mjs';
import enhancedResolve from 'enhanced-resolve';
const resolver = enhancedResolve.create.sync({
extensionAlias: {
'.js': ['.ts', '.tsx', '.js'],
},
});
/**
* @param {string} fromDir
* @param {string} moduleName
*/
function resolveFrom(fromDir, moduleName) {
try {
const result = resolver(fromDir, moduleName);
if (result === false) {
return null;
}
return result;
} catch (error) {
return null;
}
}
/**
* @param {string | string[]} input
* @returns {string[]}
*/
function toArray(input) {
return Array.isArray(input) ? input : [input];
}
/**
* @param {string} filePath
* @param {string} target
*/
function containsPath(filePath, target) {
const relative = path.relative(target, filePath);
return relative === '' || !relative.startsWith('..');
}
/**
* @param {string} fileName
* @param {RegExp | string} targetPath
*/
function isMatchingTargetPath(fileName, targetPath) {
return typeof targetPath === 'string'
? containsPath(fileName, targetPath)
: targetPath.test(fileName);
}
/** @type {Map<string, RegExp | string>} */
const REGEX_CACHE = new Map();
/**
* @typedef {object} Zone
* @property {string | string[]=} target
* @property {string | string[]=} from
* @property {string[]=} except
* @property {string=} message
*/
/**
* @typedef {object} Matcher
* @property {(RegExp | string)[]} targetPaths
* @property {(RegExp | string)[]} fromPaths
* @property {(RegExp | string)[] | null} exceptPaths
* @property {string | null} message
*/
/** @type {[Options]} */
const defaultOptions = [{}];
/**
* @typedef {object} Options
* @property {Zone[]=} zones
* @property {string=} basePath
*/
export const noRestrictedPaths = ESLintUtils.RuleCreator.withoutDocs({
name: 'no-restricted-paths',
meta: {
type: 'problem',
messages: {
pathRestrictedNoMessage:
'Unexpected path "{{moduleName}}" imported in restricted zone.',
pathRestrictedWithMessage:
'Unexpected path "{{moduleName}}" imported in restricted zone. {{message}}',
},
schema: [
{
type: 'object',
properties: {
zones: {
type: 'array',
minItems: 1,
items: {
type: 'object',
properties: {
target: {
anyOf: [
{ type: 'string' },
{
type: 'array',
items: { type: 'string' },
uniqueItems: true,
minItems: 1,
},
],
},
from: {
anyOf: [
{ type: 'string' },
{
type: 'array',
items: { type: 'string' },
uniqueItems: true,
minItems: 1,
},
],
},
except: {
type: 'array',
items: {
type: 'string',
},
uniqueItems: true,
},
message: { type: 'string' },
},
additionalProperties: false,
},
},
basePath: { type: 'string' },
},
additionalProperties: false,
},
],
defaultOptions,
},
create(context) {
const { filename, sourceCode } = context;
const zones = context.options[0]?.zones ?? [];
const basePath = context.options[0]?.basePath ?? context.cwd;
const matchers = zones.map(zone => {
assert(zone.target != null, 'Zone missing `target`');
assert(zone.from != null, 'Zone missing `from`');
const zoneTarget = toArray(zone.target);
const zoneFrom = toArray(zone.from);
assert(zoneTarget.length > 0, 'Zone needs at least one `target`');
assert(zoneFrom.length > 0, 'Zone needs at least one `from`');
let zoneExcept = zone.except != null ? toArray(zone.except) : null;
if (zoneExcept?.length === 0) {
zoneExcept = null;
}
let hasGlobPatterns = false;
let hasNonGlobPatterns = false;
/** @param {string} target */
function compilePattern(target) {
const targetPath = path.resolve(basePath, target);
const cached = REGEX_CACHE.get(targetPath);
if (cached != null) {
return cached;
}
/** @type {RegExp | string} */
let result;
if (isGlob(targetPath)) {
hasGlobPatterns = true;
result = micromatch.makeRe(targetPath);
} else {
hasNonGlobPatterns = true;
result = targetPath;
}
if (hasGlobPatterns && hasNonGlobPatterns) {
throw new Error(
'Cannot have both glob and non-glob patterns in the same zone'
);
}
REGEX_CACHE.set(targetPath, result);
return result;
}
/** @type {Matcher} */
const matcher = {
targetPaths: zoneTarget.map(target => compilePattern(target)),
fromPaths: zoneFrom.map(from => compilePattern(from)),
exceptPaths: zoneExcept?.map(except => compilePattern(except)) ?? null,
message: zone.message ?? null,
};
return matcher;
});
const targetMatchers = matchers.filter(matcher => {
return matcher.targetPaths.some(targetPath => {
return isMatchingTargetPath(filename, targetPath);
});
});
if (targetMatchers.length === 0) {
return {};
}
return createImportSourceVisitor(sourceCode, source => {
const dirname = path.dirname(filename);
const moduleName = source.value;
const resolvedPath = resolveFrom(dirname, moduleName);
if (resolvedPath == null) {
return;
}
for (const matcher of targetMatchers) {
const matchesFromPath = matcher.fromPaths.some(fromPath => {
return isMatchingTargetPath(resolvedPath, fromPath);
});
if (!matchesFromPath) {
continue;
}
const matchesExceptPath = matcher.exceptPaths?.some(exceptPath => {
return isMatchingTargetPath(resolvedPath, exceptPath);
});
if (matchesExceptPath) {
continue;
}
if (matcher.message != null) {
context.report({
node: source,
messageId: 'pathRestrictedWithMessage',
data: { moduleName, message: matcher.message },
});
} else {
context.report({
node: source,
messageId: 'pathRestrictedNoMessage',
data: { moduleName },
});
}
}
});
},
});
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { noRestrictedPaths } from './noRestrictedPaths.mjs';
import { RuleTester } from '@typescript-eslint/rule-tester';
import * as path from 'node:path';
const basePath = path.join(import.meta.dirname, 'fixtures/noRestrictedPaths');
const filename = path.join(basePath, 'client/entry.ts');
/**
* @param {boolean=} withMessage
* @returns {[import("./noRestrictedPaths.mjs").Options]}
*/
function opts(withMessage) {
const message = withMessage ? 'just stop it' : undefined;
return [
{ basePath, zones: [{ target: './client', from: './server', message }] },
];
}
const ruleTester = new RuleTester();
ruleTester.run('no-restricted-paths', noRestrictedPaths, {
valid: [
{ filename, options: opts(), code: `import b from './client.ts';` },
{ filename, options: opts(), code: `import b from './client.js';` },
{ filename, options: opts(), code: `import b from '../client/client.ts';` },
{ filename, options: opts(), code: `import b from './nonexistant';` },
{ filename, options: opts(), code: `import b from 'node:path';` },
{ filename, options: opts(), code: `import b from 'react';` },
{ filename, options: opts(), code: `import b from 'fake-module';` },
],
invalid: [
{
filename,
options: opts(),
code: `import b from '../server/server.ts';`,
errors: [{ messageId: 'pathRestrictedNoMessage' }],
},
{
filename,
options: opts(),
code: `import b from '../server/server.js';`,
errors: [{ messageId: 'pathRestrictedNoMessage' }],
},
{
filename,
options: opts(true),
code: `import b from '../server/server.ts';`,
errors: [{ messageId: 'pathRestrictedWithMessage' }],
},
],
});
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { isPropertyAccess } from './utils/astUtils.mjs';
export const noThen = ESLintUtils.RuleCreator.withoutDocs({
name: 'no-then',
meta: {
type: 'problem',
messages: {
preferAwait: 'Prefer await instead of .then()',
},
schema: [],
defaultOptions: [],
},
create(context) {
return {
MemberExpression(node) {
if (!isPropertyAccess(node, 'then')) {
return;
}
if (node.parent.type !== 'CallExpression') {
return;
}
context.report({
node: node.property,
messageId: 'preferAwait',
});
},
};
},
});
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
/**
* @param condition {unknown}
* @param message {string}
* @returns {asserts condition}
*/
export function assert(condition, message) {
if (condition == null || condition === false) {
throw new TypeError(message);
}
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
/**
* @typedef {import("@typescript-eslint/utils").TSESTree.Node} Node
* @typedef {import("@typescript-eslint/utils").TSESTree.Literal} Literal
* @typedef {import("@typescript-eslint/utils").TSESTree.StringLiteral} StringLiteral
* @typedef {import("@typescript-eslint/utils").TSESTree.Identifier} Identifier
* @typedef {import("@typescript-eslint/utils").TSESTree.MemberExpression} MemberExpression
*/
/**
* @param {Node=} node
* @returns {node is StringLiteral}
*/
export function isStringLiteral(node) {
return node?.type === 'Literal' && typeof node.value === 'string';
}
/**
* @param {Node | null | undefined} node
* @param {string} property
* @returns {node is MemberExpression}
*/
export function isPropertyAccess(node, property) {
if (node?.type !== 'MemberExpression') {
return false;
}
if (node.computed) {
return node.property.type === 'Literal' && node.property.value === property;
}
return node.property.type === 'Identifier' && node.property.name === property;
}
@@ -0,0 +1,123 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { getReferenceType } from './getReferenceType.mjs';
import { isStringLiteral } from './astUtils.mjs';
/**
* @typedef {import("@typescript-eslint/utils").TSESTree.StringLiteral} StringLiteral
* @typedef {import("@typescript-eslint/utils").TSESLint.SourceCode} SourceCode
* @typedef {import("@typescript-eslint/utils").TSESLint.RuleListener} RuleListener
*/
/**
* @param {SourceCode} sourceCode
* @param {(source: StringLiteral) => void} visitSource
* @returns {RuleListener}
*/
export function createImportSourceVisitor(sourceCode, visitSource) {
return {
// import ... from '<source>'
ImportDeclaration(node) {
visitSource(node.source);
},
// import('<source>')
ImportExpression(node) {
if (!isStringLiteral(node.source)) {
return;
}
visitSource(node.source);
},
CallExpression(node) {
// require('<source>')
if (node.callee.type === 'Identifier') {
if (node.callee.name !== 'require') {
return;
}
const refType = getReferenceType(sourceCode, node.callee);
if (refType != null && refType !== 'global') {
return;
}
const arg = node.arguments.at(0);
if (!isStringLiteral(arg)) {
return;
}
visitSource(arg);
return;
}
// require.resolve('<source>')
if (node.callee.type === 'MemberExpression') {
const { object, property } = node.callee;
if (object.type !== 'Identifier') {
return;
}
if (object.name !== 'require') {
return;
}
const refType = getReferenceType(sourceCode, object);
if (refType != null && refType !== 'global') {
return;
}
if (property.type !== 'Identifier') {
return;
}
if (property.name !== 'resolve') {
return;
}
const arg = node.arguments.at(0);
if (!isStringLiteral(arg)) {
return;
}
visitSource(arg);
}
},
// import.meta.resolve('<source>')
MetaProperty(node) {
if (node.meta.name !== 'import') {
return;
}
if (node.property.name !== 'meta') {
return;
}
const memberExpression = node.parent;
if (memberExpression.type !== 'MemberExpression') {
return;
}
if (memberExpression.property.type !== 'Identifier') {
return;
}
if (memberExpression.property.name !== 'resolve') {
return;
}
const callExpression = memberExpression.parent;
if (callExpression.type !== 'CallExpression') {
return;
}
const arg = callExpression.arguments.at(0);
if (!isStringLiteral(arg)) {
return;
}
visitSource(arg);
},
// export {...} from '<source>'
ExportNamedDeclaration(node) {
if (node.source == null) {
return;
}
visitSource(node.source);
},
// export * ... from '<source>'
ExportAllDeclaration(node) {
visitSource(node.source);
},
};
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
/**
* @typedef {import("@typescript-eslint/utils").TSESTree.Identifier} Identifier
* @typedef {import("@typescript-eslint/utils").TSESLint.SourceCode} SourceCode
*/
/**
* @param {SourceCode} sourceCode
* @param {Identifier} node
*/
export function getReferenceType(sourceCode, node) {
const scope = sourceCode.getScope(node);
const ref = scope.references.find(r => r.identifier === node);
return ref?.resolved?.scope.type ?? null;
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as mocha from 'mocha';
import { RuleTester } from '@typescript-eslint/rule-tester';
RuleTester.afterAll = mocha.after;
+1816
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -14,7 +14,6 @@ libtextsecure/test/test.js
stylesheets/*.css
test/test.js
ts/**/*.js
!ts/**/.eslintrc.js
ts/protobuf/*.d.ts
ts/protobuf/*.js
stylesheets/manifest.css
+20 -12
View File
@@ -11,7 +11,7 @@ const EXTERNALS = new Set(builtinModules);
EXTERNALS.delete('buffer');
EXTERNALS.delete('url');
const config: StorybookConfig = {
const storybookConfig: StorybookConfig = {
typescript: {
reactDocgen: false,
},
@@ -53,16 +53,19 @@ const config: StorybookConfig = {
},
],
webpackFinal(config) {
config.cache = {
webpackFinal(webpackConfig) {
// oxlint-disable-next-line no-param-reassign
webpackConfig.cache = {
type: 'filesystem',
};
config.resolve!.extensionAlias = {
// oxlint-disable-next-line no-param-reassign, typescript/no-non-null-assertion
webpackConfig.resolve!.extensionAlias = {
'.js': ['.tsx', '.ts', '.js'],
};
config.module!.rules!.unshift({
// oxlint-disable-next-line typescript/no-non-null-assertion
webpackConfig.module!.rules!.unshift({
test: /\.scss$/,
use: [
{ loader: 'style-loader' },
@@ -71,14 +74,16 @@ const config: StorybookConfig = {
],
});
config.module!.rules!.unshift({
// oxlint-disable-next-line typescript/no-non-null-assertion
webpackConfig.module!.rules!.unshift({
test: /\.css$/,
use: [
// prevent storybook defaults from being applied
],
});
config.module!.rules!.push({
// oxlint-disable-next-line typescript/no-non-null-assertion
webpackConfig.module!.rules!.push({
test: /tailwind-config\.css$/,
use: [
{
@@ -95,9 +100,11 @@ const config: StorybookConfig = {
],
});
config.node = { global: true };
// oxlint-disable-next-line no-param-reassign
webpackConfig.node = { global: true };
config.externals = ({ request }, callback) => {
// oxlint-disable-next-line no-param-reassign
webpackConfig.externals = ({ request }, callback) => {
if (
(/^node:/.test(request) && request !== 'node:buffer') ||
EXTERNALS.has(request)
@@ -108,16 +115,17 @@ const config: StorybookConfig = {
callback();
};
config.plugins!.push(
// oxlint-disable-next-line typescript/no-non-null-assertion
webpackConfig.plugins!.push(
new ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
})
);
return config;
return webpackConfig;
},
docs: {},
};
export default config;
export default storybookConfig;
+8 -3
View File
@@ -11,7 +11,8 @@ import * as styles from './styles.scss';
import messages from '../_locales/en/messages.json';
import { Provider } from 'react-redux';
import { Store, combineReducers, createStore } from 'redux';
import type { Store } from 'redux';
import { combineReducers, createStore } from 'redux';
import { Globals } from '@react-spring/web';
import { StorybookThemeContext } from './StorybookThemeContext.std.js';
@@ -100,7 +101,7 @@ const mockStore: Store<StateType> = createStore(
})
);
// eslint-disable-next-line
// oxlint-disable-next-line
const noop = () => {};
window.Whisper = window.Whisper || {};
@@ -138,6 +139,7 @@ window.SignalContext = {
platform: '',
release: '',
},
// oxlint-disable-next-line typescript/no-explicit-any
config: {} as any,
getHourCyclePreference: () => HourCyclePreference.UnknownPreference,
@@ -241,7 +243,7 @@ function withMockStoreProvider(Story, context) {
function withScrollLockProvider(Story, context) {
return (
<ScrollerLockContext.Provider
value={createScrollerLock('MockStories', () => {})}
value={createScrollerLock('MockStories', () => null)}
>
<Story {...context} />
</ScrollerLockContext.Provider>
@@ -265,12 +267,15 @@ function withFunProvider(Story, context) {
fetchGifsFeatured={() => Promise.resolve(MOCK_GIFS_PAGINATED_ONE_PAGE)}
fetchGif={() => Promise.resolve(new Blob([new Uint8Array(1)]))}
onSelectEmoji={function (emojiSelection: FunEmojiSelection): void {
// oxlint-disable-next-line no-console
console.log('onSelectEmoji', emojiSelection);
}}
onSelectSticker={function (stickerSelection: FunStickerSelection): void {
// oxlint-disable-next-line no-console
console.log('onSelectSticker', stickerSelection);
}}
onSelectGif={function (gifSelection: FunGifSelection): void {
// oxlint-disable-next-line no-console
console.log('onSelectGif', gifSelection);
}}
>
+5
View File
@@ -54,16 +54,20 @@ const config: TestRunnerConfig = {
.or(page.getByTitle(value))
.or(page.getByLabel(value));
// oxlint-disable-next-line no-await-in-loop
if (await locator.count()) {
const first = locator.first();
try {
// oxlint-disable-next-line no-await-in-loop
await first.focus({ timeout: SECOND });
} catch {
// Opportunistic
}
try {
// oxlint-disable-next-line no-await-in-loop
if (await first.isVisible()) {
// oxlint-disable-next-line no-await-in-loop
await first.scrollIntoViewIfNeeded({ timeout: SECOND });
}
} catch {
@@ -71,6 +75,7 @@ const config: TestRunnerConfig = {
}
}
// oxlint-disable-next-line no-await-in-loop
const image = await page.screenshot({
animations: 'disabled',
fullPage: true,
+2 -2
View File
@@ -418,7 +418,7 @@ function deleteOrphanedAttachments({
let attachments: ReadonlyArray<string>;
let downloads: ReadonlyArray<string>;
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
({ attachments, downloads, cursor } = await sql.sqlRead(
'getKnownMessageAttachments',
cursor
@@ -443,7 +443,7 @@ function deleteOrphanedAttachments({
// Let other SQL calls come through. There are hundreds of thousands of
// messages in the database and it might take time to go through them all.
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await sleep(INTERACTIVITY_DELAY);
} while (cursor !== undefined && !cursor.done);
} finally {
+2 -2
View File
@@ -240,7 +240,7 @@ export const deleteAllBadges = async ({
let filesDeleted = 0;
for (const file of await getAllBadgeImageFiles(userDataPath)) {
if (!pathsToKeep.has(file)) {
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await deleteFromDisk(file);
filesDeleted += 1;
}
@@ -261,7 +261,7 @@ export const deleteAllMegaphones = async ({
let filesDeleted = 0;
for (const file of await getAllMegaphoneImageFiles(userDataPath)) {
if (!pathsToKeep.has(file)) {
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await deleteFromDisk(file);
filesDeleted += 1;
}
+2 -1
View File
@@ -53,7 +53,7 @@ process.env.NODE_CONFIG_DIR = join(getAppRootDir(), 'config');
// Note: we use `require()` because esbuild moves the imports to the top of
// the module regardless of their actual placement in the file.
// See: https://github.com/evanw/esbuild/issues/2011
// eslint-disable-next-line @typescript-eslint/no-var-requires
// oxlint-disable-next-line typescript/no-var-requires
const config: IConfig = require('config');
if (getEnvironment() !== Environment.PackagedApp) {
@@ -73,6 +73,7 @@ if (getEnvironment() !== Environment.PackagedApp) {
'SUPPRESS_NO_CONFIG_WARNING',
'SIGNAL_ENABLE_HTTP',
].forEach(s => {
// oxlint-disable-next-line no-console
console.log(`${s} ${config.util.getEnv(s)}`);
});
+1
View File
@@ -18,6 +18,7 @@ let copyErrorAndQuitText = 'Copy error and quit';
function handleError(prefix: string, error: Error): void {
const formattedError = Errors.toLogFormat(error);
// oxlint-disable-next-line no-console
console.error(`${prefix}:`, formattedError);
log.error(`${prefix}:`, formattedError);
+19 -16
View File
@@ -384,7 +384,7 @@ async function getBackgroundColor(
async function getLocaleOverrideSetting(): Promise<string | null> {
const value = ephemeralConfig.get('localeOverride');
// eslint-disable-next-line eqeqeq -- Checking for null explicitly
// oxlint-disable-next-line eqeqeq -- Checking for null explicitly
if (typeof value === 'string' || value === null) {
log.info('got fast localeOverride setting', value);
return value;
@@ -818,13 +818,13 @@ async function createWindow() {
maximized: mainWindow.isMaximized(),
autoHideMenuBar: mainWindow.autoHideMenuBar,
fullscreen: mainWindow.isFullScreen(),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
width: size[0]!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
height: size[1]!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
x: position[0]!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
y: position[1]!,
};
@@ -1555,7 +1555,7 @@ async function showCallDiagnosticWindow() {
let permissionsPopupWindow: BrowserWindow | undefined;
function showPermissionsPopupWindow(forCalling: boolean, forCamera: boolean) {
// eslint-disable-next-line no-async-promise-executor
// oxlint-disable-next-line no-async-promise-executor
return new Promise<void>(async (resolveFn, reject) => {
if (permissionsPopupWindow) {
permissionsPopupWindow.show();
@@ -1569,9 +1569,9 @@ function showPermissionsPopupWindow(forCalling: boolean, forCamera: boolean) {
const size = mainWindow.getSize();
const options = {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
width: Math.min(400, size[0]!),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
height: Math.min(150, size[1]!),
resizable: false,
title: getResolvedMessagesLocale().i18n('icu:allowAccess'),
@@ -1683,6 +1683,7 @@ function getSQLKey(): string {
typeof previousBackend === 'string' &&
previousBackend !== safeStorageBackend
) {
// oxlint-disable-next-line no-console
console.error(
`Detected change in safeStorage backend, can't decrypt DB key (previous: ${previousBackend}, current: ${safeStorageBackend})`
);
@@ -2271,7 +2272,7 @@ app.on('ready', async () => {
});
drop(
// eslint-disable-next-line more/no-then
// oxlint-disable-next-line promise/prefer-await-to-then, signal-desktop/no-then
Promise.race([sqlInitPromise, timeout]).then(async maybeTimeout => {
if (maybeTimeout !== 'timeout') {
return;
@@ -2469,6 +2470,7 @@ async function maybeRequestCloseConfirmation(): Promise<boolean> {
'maybeRequestCloseConfirmation: Checking to see if close confirmation is needed'
);
const request = new Promise<boolean>(resolveFn => {
// oxlint-disable-next-line prefer-const
let timeout: NodeJS.Timeout | undefined;
if (!mainWindow) {
@@ -2520,6 +2522,7 @@ async function requestShutdown() {
log.info('requestShutdown: Requesting close of mainWindow...');
const request = new Promise<void>(resolveFn => {
// oxlint-disable-next-line prefer-const
let timeout: NodeJS.Timeout | undefined;
if (!mainWindow) {
@@ -2937,31 +2940,31 @@ ipc.on('get-config', async event => {
);
}
// eslint-disable-next-line no-param-reassign
// oxlint-disable-next-line no-param-reassign
event.returnValue = parsed.data;
});
// Ingested in preload.js via a sendSync call
ipc.on('locale-data', event => {
// eslint-disable-next-line no-param-reassign
// oxlint-disable-next-line no-param-reassign
event.returnValue = getResolvedMessagesLocale().messages;
});
// Ingested in preload.js via a sendSync call
ipc.on('locale-display-names', event => {
// eslint-disable-next-line no-param-reassign
// oxlint-disable-next-line no-param-reassign
event.returnValue = getResolvedMessagesLocale().localeDisplayNames;
});
// Ingested in preload.js via a sendSync call
ipc.on('country-display-names', event => {
// eslint-disable-next-line no-param-reassign
// oxlint-disable-next-line no-param-reassign
event.returnValue = getResolvedMessagesLocale().countryDisplayNames;
});
// TODO DESKTOP-5241
ipc.on('OS.getClassName', event => {
// eslint-disable-next-line no-param-reassign
// oxlint-disable-next-line no-param-reassign
event.returnValue = OS.getClassName();
});
@@ -2990,7 +2993,7 @@ ipc.handle('DebugLogs.upload', async (_event, content: string) => {
});
ipc.on('get-user-data-path', event => {
// eslint-disable-next-line no-param-reassign
// oxlint-disable-next-line no-param-reassign
event.returnValue = app.getPath('userData');
});
@@ -3442,7 +3445,7 @@ async function showStickerCreatorWindow() {
if (isTestEnvironment(getEnvironment())) {
ipc.on('ci:test-electron:getArgv', event => {
// eslint-disable-next-line no-param-reassign
// oxlint-disable-next-line no-param-reassign
event.returnValue = process.argv;
});
+2
View File
@@ -27,6 +27,7 @@ if (userData !== undefined) {
try {
mkdirSync(userData, { recursive: true });
} catch (error) {
// oxlint-disable-next-line no-console
console.error('Failed to create userData', Errors.toLogFormat(error));
}
@@ -34,6 +35,7 @@ if (userData !== undefined) {
}
// Use console.log because logger isn't fully initialized yet
// oxlint-disable-next-line no-console
console.log(`userData: ${app.getPath('userData')}`);
const userDataPath = app.getPath('userData');
+6 -5
View File
@@ -174,7 +174,7 @@ async function lintMessages() {
const key = topProp.key.value;
if (process.argv.includes('--test')) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
const test = tests[key]!;
const actualErrors = reports.map(report => report.id);
deepEqual(actualErrors, test.expectErrors);
@@ -197,13 +197,13 @@ async function lintMessages() {
loc = `:${line}:${column + report.locationOffset}`;
}
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.error(
chalk`{bold.cyan ${relativePath}${loc}} ${report.message} {magenta ({underline ${report.id}})}`
);
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.error(chalk` {dim in ${key} is "}{red ${icuMessage}}{dim "}`);
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.error();
failed = true;
@@ -215,8 +215,9 @@ async function lintMessages() {
}
}
// oxlint-disable-next-line promise/prefer-await-to-then
lintMessages().catch(error => {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.error(error);
process.exit(1);
});
+1 -1
View File
@@ -37,7 +37,7 @@ export default rule('wrapEmoji', context => {
return;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
const child = element.children[0]!;
if (!isLiteralElement(child)) {
// non-literal
+1
View File
@@ -2,6 +2,7 @@
"dependencies": {
"danger": "12.3.4",
"endanger": "7.0.4",
"semver": "7.7.4",
"typescript": "5.6.3"
},
"pnpm": {
+5 -1
View File
@@ -28,4 +28,8 @@ async function main() {
}
}
main();
// oxlint-disable-next-line promise/prefer-await-to-then
main().catch(error => {
console.error(error);
process.exit(1);
});
@@ -1,7 +1,8 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { File, Rule } from 'endanger';
import type { File } from 'endanger';
import { Rule } from 'endanger';
import semver from 'semver';
function isPinnedVersion(spec: string): boolean {
@@ -14,12 +15,13 @@ function isPinnedVersion(spec: string): boolean {
} else {
version = spec;
}
return semver.valid(version) !== null;
return semver.valid(version) != null;
}
async function getLineContaining(file: File, text: string) {
let lines = await file.lines();
for (let line of lines) {
const lines = await file.lines();
for (const line of lines) {
// oxlint-disable-next-line no-await-in-loop
if (await line.contains(text)) {
return line;
}
@@ -27,14 +29,15 @@ async function getLineContaining(file: File, text: string) {
return null;
}
let dependencyTypes = [
const dependencyTypes = [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
];
export default function packageJsonVersionsShouldBePinned() {
// oxlint-disable-next-line typescript/no-explicit-any
export default function packageJsonVersionsShouldBePinned(): Rule<any, any> {
return new Rule({
match: {
files: ['**/package.json', '!**/node_modules/**'],
@@ -47,17 +50,19 @@ export default function packageJsonVersionsShouldBePinned() {
`,
},
async run({ files, context }) {
for (let file of files.modifiedOrCreated) {
let pkg = await file.json();
for (let dependencyType of dependencyTypes) {
let deps = pkg[dependencyType];
for (const file of files.modifiedOrCreated) {
// oxlint-disable-next-line no-await-in-loop
const pkg = await file.json();
for (const dependencyType of dependencyTypes) {
const deps = pkg[dependencyType];
if (deps == null) {
continue;
}
for (let depName of Object.keys(deps)) {
let depVersion = deps[depName];
for (const depName of Object.keys(deps)) {
const depVersion = deps[depName];
if (!isPinnedVersion(depVersion)) {
let line = await getLineContaining(
// oxlint-disable-next-line no-await-in-loop
const line = await getLineContaining(
file,
`"${depName}": "${depVersion}"`
);
@@ -1,7 +1,7 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { Line, Rule } from 'endanger';
import { Rule } from 'endanger';
function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
@@ -10,9 +10,10 @@ function assert(condition: boolean, message: string): asserts condition {
}
function isObject(value: unknown): value is object {
return typeof value === 'object' && value !== null;
return typeof value === 'object' && value != null;
}
// oxlint-disable-next-line typescript/no-explicit-any
function has<T extends object, const K extends T[any]>(
value: T,
key: K
@@ -20,7 +21,8 @@ function has<T extends object, const K extends T[any]>(
return Object.hasOwn(value, key);
}
export default function pnpmLockDepsShouldHaveIntegrity() {
// oxlint-disable-next-line typescript/no-explicit-any
export default function pnpmLockDepsShouldHaveIntegrity(): Rule<any, any> {
return new Rule({
match: {
files: ['pnpm-lock.yaml'],
@@ -36,6 +38,7 @@ export default function pnpmLockDepsShouldHaveIntegrity() {
},
async run({ files, context }) {
for (const file of files.modifiedOrCreated) {
// oxlint-disable-next-line no-await-in-loop
const contents: unknown = await file.yaml();
assert(
-11
View File
@@ -1,11 +0,0 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable global-require */
module.exports = {
'license-comments': require('./.eslint/rules/license-comments'),
'type-alias-readonlydeep': require('./.eslint/rules/type-alias-readonlydeep'),
'enforce-tw': require('./.eslint/rules/enforce-tw'),
'enforce-array-buffer': require('./.eslint/rules/enforce-array-buffer'),
'file-suffix': require('./.eslint/rules/file-suffix'),
};
-12
View File
@@ -1,12 +0,0 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
module.exports = {
env: {
browser: true,
node: false,
},
parserOptions: {
sourceType: 'script',
},
};
+16 -17
View File
@@ -43,16 +43,16 @@
"prepare-adhoc-version": "node scripts/prepare_tagged_version.js adhoc",
"prepare-staging-build": "node scripts/prepare_staging_build.js",
"prepare-linux-build": "node scripts/prepare_linux_build.js",
"test": "run-s test-node test-electron test-lint-intl test-eslint",
"test": "run-s test-node test-electron test-lint-intl test-oxlint",
"test-electron": "node ts/scripts/test-electron.node.js",
"test-release": "node ts/scripts/test-release.node.js",
"test-node": "cross-env NODE_ENV=test LANG=en-us electron-mocha --timeout 10000 --file test/setup-test-node.js --recursive ts/test-node",
"test-mock": "node ts/scripts/mocha-separator.node.js --require ts/test-mock/setup-ci.node.js -- ts/test-mock/**/*_test.node.js",
"test-mock-docker": "mocha --require ts/test-mock/setup-ci.node.js ts/test-mock/**/*_test.docker.node.js",
"test-eslint": "mocha .eslint/rules/**/*.test.js --ignore-leaks",
"test-oxlint": "mocha --require .oxlint/test-setup.mjs .oxlint/rules/**/*.test.mjs",
"test-lint-intl": "ts-node ./build/intl-linter/linter.node.ts --test",
"eslint": "eslint --cache . --cache-strategy content --max-warnings 0",
"lint": "run-p --aggregate-output --print-label lint-prettier lint-css check:types eslint",
"oxlint": "oxlint",
"lint": "run-p --aggregate-output --print-label lint-prettier lint-css check:types oxlint",
"lint-deps": "node ts/util/lint/linter.node.js",
"lint-license-comments": "ts-node ts/util/lint/license_comments.node.ts",
"lint-prettier": "pprettier --check '**/*.{ts,tsx,d.ts,js,json,html,scss,md,yml,yaml}' '!node_modules/**'",
@@ -264,11 +264,13 @@
"@types/google-libphonenumber": "7.4.30",
"@types/humanize-duration": "3.18.1",
"@types/intl-tel-input": "18.1.4",
"@types/is-glob": "4.0.4",
"@types/js-yaml": "4.0.9",
"@types/json-to-ast": "2.1.4",
"@types/linkify-it": "5.0.0",
"@types/lodash": "4.14.106",
"@types/memoizee": "0.4.11",
"@types/micromatch": "4.0.10",
"@types/mocha": "10.0.9",
"@types/node": "24.12.0",
"@types/node-fetch": "2.6.12",
@@ -289,7 +291,9 @@
"@types/write-file-atomic": "4.0.3",
"@types/yargs": "17.0.33",
"@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.18.1",
"@typescript-eslint/parser": "8.57.2",
"@typescript-eslint/rule-tester": "8.57.2",
"@typescript-eslint/utils": "8.57.2",
"axe-core": "4.10.2",
"babel-core": "7.0.0-bridge.0",
"babel-loader": "9.2.1",
@@ -312,25 +316,22 @@
"enhanced-resolve": "5.18.3",
"enquirer": "2.4.1",
"esbuild": "0.25.9",
"eslint": "8.56.0",
"eslint-config-airbnb-typescript-prettier": "5.0.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-better-tailwindcss": "3.7.2",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-local-rules": "1.3.2",
"eslint-plugin-mocha": "10.1.0",
"eslint-plugin-more": "1.0.5",
"eslint-plugin-react": "7.31.10",
"eslint": "10.1.0",
"eslint-plugin-better-tailwindcss": "4.3.2",
"execa": "5.1.1",
"html-webpack-plugin": "5.6.3",
"http-server": "14.1.1",
"is-glob": "4.0.3",
"json-to-ast": "2.1.0",
"log-symbols": "4.1.0",
"micromatch": "4.0.8",
"mini-css-extract-plugin": "2.9.2",
"mocha": "10.8.2",
"node-gyp": "11.0.0",
"node-gyp-build": "4.8.4",
"npm-run-all": "4.1.5",
"oxlint": "1.57.0",
"oxlint-tsgolint": "0.17.4",
"p-limit": "3.1.0",
"pe-library": "2.0.1",
"pixelmatch": "5.3.0",
@@ -354,7 +355,7 @@
"stylelint-use-logical-spec": "5.0.1",
"svgo": "3.3.2",
"synckit": "0.11.11",
"tailwindcss": "4.1.7",
"tailwindcss": "4.2.1",
"terser-webpack-plugin": "5.3.10",
"ts-node": "10.9.2",
"typescript": "5.9.3",
@@ -366,7 +367,6 @@
"pnpm": {
"overrides": {
"@storybook/core>node-fetch": "2.6.7",
"eslint-config-airbnb-typescript-prettier>eslint-plugin-prettier": "5.2.1",
"canvas": "-",
"jsdom": "-",
"thenify-all>thenify": "3.3.1",
@@ -647,7 +647,6 @@
"!**/.{github,husky,grenrc,npmrc,nycrc,taprc,bithoundrc}",
"!**/.testem.json",
"!**/.babelrc*",
"!**/.eslintrc*",
"!**/.prettier*",
"!**/.jscs*",
"!**/*travis*.yml",
+742 -1095
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -2,14 +2,14 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { readFileSync, writeFileSync } from 'node:fs';
import { join, dirname, basename } from 'node:path';
import { join } from 'node:path';
import { Script, constants } from 'node:vm';
import { ipcRenderer } from 'electron';
const srcPath = join(__dirname, 'preload.bundle.js');
const cachePath = join(__dirname, 'preload.bundle.cache');
let cachedData: Buffer | undefined;
let cachedData: Buffer<ArrayBuffer> | undefined;
try {
if (!process.env.GENERATE_PRELOAD_CACHE) {
cachedData = readFileSync(cachePath);
@@ -76,7 +76,9 @@ if (process.env.GENERATE_PRELOAD_CACHE) {
}
if (cachedDataRejected) {
// oxlint-disable-next-line no-console
console.log('preload cache rejected');
} else {
// oxlint-disable-next-line no-console
console.log('preload cache hit');
}
+5 -1
View File
@@ -16,7 +16,6 @@ const PATTERNS = [
'tsconfig.tsbuildinfo',
'preload.bundle.js',
'preload.bundle.cache',
'.eslintcache',
];
async function main() {
@@ -25,12 +24,17 @@ async function main() {
});
const promises = [];
let count = 0;
for await (const entry of readable) {
count += 1;
promises.push(rm(entry, { recursive: true, force: true }));
}
await Promise.all(promises);
console.log(`Deleted ${count} files`);
}
// oxlint-disable-next-line promise/prefer-await-to-then
main().catch(error => {
console.error(error);
process.exit(1);
+1
View File
@@ -205,6 +205,7 @@ async function main() {
]);
}
// oxlint-disable-next-line promise/prefer-await-to-then
main().catch(error => {
console.error(error.stack);
process.exit(1);
+1
View File
@@ -167,6 +167,7 @@ async function main() {
await fs.promises.writeFile(destinationPath, output);
}
// oxlint-disable-next-line promise/prefer-await-to-then
main().catch(err => {
console.error(err);
process.exit(1);
-36
View File
@@ -1,36 +0,0 @@
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// For reference: https://github.com/airbnb/javascript
module.exports = {
env: {
mocha: true,
browser: true,
},
globals: {
assert: true,
getString: true,
},
parserOptions: {
sourceType: 'script',
},
rules: {
// We still get the value of this rule, it just allows for dev deps
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: true,
},
],
// We want to keep each test structured the same, even if its contents are tiny
'arrow-body-style': 'off',
strict: 'off',
'more/no-then': 'off',
},
};
+3 -3
View File
@@ -43,11 +43,11 @@ global.navigator = {};
global.WebSocket = {};
// For GlobalAudioContext.tsx
/* eslint max-classes-per-file: ["error", 2] */
global.AudioContext = class {};
// oxlint-disable-next-line max-classes-per-file
global.Audio = class {
// eslint-disable-next-line @typescript-eslint/no-empty-function
// oxlint-disable-next-line typescript/no-empty-function
pause() {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
// oxlint-disable-next-line typescript/no-empty-function
addEventListener() {}
};
+2
View File
@@ -26,7 +26,9 @@ before(async () => {
window.testUtilities.prepareTests();
delete window.testUtilities.prepareTests;
// oxlint-disable-next-line no-unused-expressions
!(function () {
// oxlint-disable-next-line no-undef
class Reporter extends Mocha.reporters.HTML {
constructor(runner, options) {
super(runner, options);
+4 -4
View File
@@ -122,7 +122,7 @@ export async function benchmarkConversationOpen({
'CI not enabled; ensure this is a staging build'
);
// eslint-disable-next-line no-param-reassign
// oxlint-disable-next-line no-param-reassign
conversationId =
conversationId || getSelectedConversationId(window.reduxStore.getState());
@@ -150,12 +150,12 @@ export async function benchmarkConversationOpen({
const durations: Array<number> = [];
for (let i = 0; i < runCount; i += 1) {
// Give some buffer between tests
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await sleep(BUFFER_DELAY_MS);
log.info(`${logId}: running open test run ${i + 1}/${runCount}`);
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
const duration = await timeConversationOpen(conversationId);
if (i >= runCountToSkip) {
@@ -184,7 +184,7 @@ async function waitForSelector(
return element;
}
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await sleep(BUFFER_DELAY_MS);
}
+13 -13
View File
@@ -38,7 +38,6 @@ import {
} from './types/SignalConversation.std.js';
import { getTitleNoDefault } from './util/getTitle.preload.js';
import * as StorageService from './services/storage.preload.js';
import textsecureUtils from './textsecure/Helpers.std.js';
import { cdsLookup } from './textsecure/WebAPI.preload.js';
import type { ConversationPropsForUnreadStats } from './util/countUnreadStats.std.js';
import { countAllConversationsUnreadStats } from './util/countUnreadStats.std.js';
@@ -67,6 +66,7 @@ import type {
} from './types/ServiceId.std.js';
import { itemStorage } from './textsecure/Storage.preload.js';
import { getSelectedConversationId } from './state/selectors/nav.std.js';
import { unencodeNumber } from './util/unencodeNumber.std.js';
const { debounce, pick, uniq, without } = lodash;
@@ -278,7 +278,7 @@ export class ConversationController {
return;
}
// eslint-disable-next-line no-param-reassign
// oxlint-disable-next-line no-param-reassign
conversation.cachedProps = undefined;
const hasAttributeChanged = (name: keyof ConversationAttributesType) => {
@@ -634,7 +634,7 @@ export class ConversationController {
return null;
}
const [id] = textsecureUtils.unencodeNumber(address);
const [id] = unencodeNumber(address);
const conv = this.get(id);
if (conv) {
@@ -1130,7 +1130,7 @@ export class ConversationController {
// Keep the newer one if it has an e164, otherwise keep existing
if (conversation.get('e164')) {
// Keep new one
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await this.#doCombineConversations({
current: conversation,
obsolete: existing,
@@ -1138,7 +1138,7 @@ export class ConversationController {
byServiceId[serviceId] = conversation;
} else {
// Keep existing - note that this applies if neither had an e164
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await this.#doCombineConversations({
current: existing,
obsolete: conversation,
@@ -1164,7 +1164,7 @@ export class ConversationController {
// Keep the newer one if it has additional data, otherwise keep existing
if (conversation.get('e164') || conversation.getPni()) {
// Keep new one
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await this.#doCombineConversations({
current: conversation,
obsolete: existing,
@@ -1172,7 +1172,7 @@ export class ConversationController {
byServiceId[pni] = conversation;
} else {
// Keep existing - note that this applies if neither had an e164
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await this.#doCombineConversations({
current: existing,
obsolete: conversation,
@@ -1210,7 +1210,7 @@ export class ConversationController {
// Keep the newer one if it has a service id, otherwise keep existing
if (conversation.getServiceId()) {
// Keep new one
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await this.#doCombineConversations({
current: conversation,
obsolete: existing,
@@ -1218,7 +1218,7 @@ export class ConversationController {
byE164[e164] = conversation;
} else {
// Keep existing - note that this applies if neither had a service id
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await this.#doCombineConversations({
current: existing,
obsolete: conversation,
@@ -1256,14 +1256,14 @@ export class ConversationController {
isGroupV2(conversation.attributes) &&
!isGroupV2(existing.attributes)
) {
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await this.#doCombineConversations({
current: conversation,
obsolete: existing,
});
byGroupV2Id[groupV2Id] = conversation;
} else {
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await this.#doCombineConversations({
current: existing,
obsolete: conversation,
@@ -1598,7 +1598,7 @@ export class ConversationController {
}
if (count % 10 === 0) {
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await sleep(300);
}
}
@@ -1719,7 +1719,7 @@ export class ConversationController {
continue;
}
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await removeConversation(convo.id);
this.#removeConversation(convo);
}
+7 -7
View File
@@ -370,7 +370,7 @@ export function verifyHmacSha256(
let result = 0;
for (let i = 0; i < theirMac.byteLength; i += 1) {
// eslint-disable-next-line no-bitwise, @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line no-bitwise, typescript/no-non-null-assertion
result |= ourMac[i]! ^ theirMac[i]!;
}
if (result !== 0) {
@@ -465,7 +465,7 @@ export function getZeroes(n: number): Uint8Array<ArrayBuffer> {
}
export function highBitsToInt(byte: number): number {
// eslint-disable-next-line no-bitwise
// oxlint-disable-next-line no-bitwise
return (byte & 0xff) >> 4;
}
@@ -473,7 +473,7 @@ export function intsToByteHighAndLow(
highValue: number,
lowValue: number
): number {
// eslint-disable-next-line no-bitwise
// oxlint-disable-next-line no-bitwise
return ((highValue << 4) | lowValue) & 0xff;
}
@@ -503,7 +503,7 @@ function verifyDigest(
const ourDigest = sha256(data);
let result = 0;
for (let i = 0; i < theirDigest.byteLength; i += 1) {
// eslint-disable-next-line no-bitwise, @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line no-bitwise, typescript/no-non-null-assertion
result |= ourDigest[i]! ^ theirDigest[i]!;
}
if (result !== 0) {
@@ -777,7 +777,7 @@ export function getIdentifierHash({
}
const digest = hash(HashType.size256, identifier);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
return digest[0]!;
}
@@ -795,10 +795,10 @@ export function generateAvatarColor({
const hashValue = getIdentifierHash({ aci, e164, pni, groupId });
if (hashValue == null) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
return sample(AvatarColors) || AvatarColors[0]!;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
return AvatarColors[hashValue % AVATAR_COLOR_COUNT]!;
}
+4 -5
View File
@@ -146,16 +146,15 @@ function validatePubKeyFormat(
}
export function setPublicKeyTypeByte(publicKey: Uint8Array<ArrayBuffer>): void {
// eslint-disable-next-line no-param-reassign
// oxlint-disable-next-line no-param-reassign
publicKey[0] = 5;
}
export function clampPrivateKey(privateKey: Uint8Array<ArrayBuffer>): void {
/* eslint-disable no-bitwise, no-param-reassign */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
// oxlint-disable-next-line no-bitwise, no-param-reassign, typescript/no-non-null-assertion
privateKey[0]! &= 248;
// oxlint-disable-next-line no-bitwise, no-param-reassign, typescript/no-non-null-assertion
privateKey[31]! &= 127;
// oxlint-disable-next-line no-bitwise, no-param-reassign, typescript/no-non-null-assertion
privateKey[31]! |= 64;
/* eslint-enable no-bitwise, no-param-reassign */
/* eslint-enable @typescript-eslint/no-non-null-assertion */
}
-7
View File
@@ -1,7 +0,0 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// https://github.com/microsoft/TypeScript/issues/29129
declare namespace Intl {
function getCanonicalLocales(locales: string | Array<string>): Array<string>;
}
+1 -2
View File
@@ -1,8 +1,6 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import lodash from 'lodash';
import type {
@@ -108,6 +106,7 @@ export type IdentityKeysOptions = Readonly<{
zone?: Zone;
}>;
// oxlint-disable-next-line max-classes-per-file
export class IdentityKeys extends IdentityKeyStore {
readonly #signalProtocolStore: SignalProtocolStore;
readonly #ourServiceId: ServiceIdString;
+7 -7
View File
@@ -186,7 +186,7 @@ async function _fillCaches<ID, T extends HasIdType<ID>, HydratedType>(
}
log.info(`Finished caching ${field} data`);
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
// oxlint-disable-next-line no-param-reassign, typescript/no-explicit-any
object[field] = cache as any;
}
@@ -2090,11 +2090,11 @@ export class SignalProtocolStore extends EventEmitter {
throw new Error('saveIdentity: encodedAddress was undefined/null');
}
if (!(publicKey instanceof Uint8Array)) {
// eslint-disable-next-line no-param-reassign
// oxlint-disable-next-line no-param-reassign
publicKey = Bytes.fromBinary(publicKey);
}
if (typeof nonblockingApproval !== 'boolean') {
// eslint-disable-next-line no-param-reassign
// oxlint-disable-next-line no-param-reassign
nonblockingApproval = false;
}
@@ -2636,14 +2636,14 @@ export class SignalProtocolStore extends EventEmitter {
// Update database
await Promise.all<void>([
itemStorage.put('identityKeyMap', {
...(itemStorage.get('identityKeyMap') || {}),
...itemStorage.get('identityKeyMap'),
[pni]: {
pubKey: pniPublicKey,
privKey: pniPrivateKey,
},
}),
itemStorage.put('registrationIdMap', {
...(itemStorage.get('registrationIdMap') || {}),
...itemStorage.get('registrationIdMap'),
[pni]: registrationId,
}),
(async () => {
@@ -2851,7 +2851,7 @@ export class SignalProtocolStore extends EventEmitter {
public override on(
eventName: string | symbol,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
listener: (...args: Array<any>) => void
): this {
return super.on(eventName, listener);
@@ -2877,7 +2877,7 @@ export class SignalProtocolStore extends EventEmitter {
public override emit(
eventName: string | symbol,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
...args: Array<any>
): boolean {
return super.emit(eventName, ...args);
+2 -1
View File
@@ -79,7 +79,7 @@ export class WebAudioRecorder {
this.#input.connect(this.processor);
this.processor.connect(this.#context.destination);
this.processor.onaudioprocess = event => {
// eslint-disable-next-line no-plusplus
// oxlint-disable-next-line no-plusplus
for (let ch = 0; ch < numChannels; ++ch) {
buffer[ch] = event.inputBuffer.getChannelData(ch);
}
@@ -130,6 +130,7 @@ export class WebAudioRecorder {
this.worker.terminate();
}
// oxlint-disable-next-line no-undef FIXME
this.worker = new Worker('js/WebAudioRecorderMp3.js');
this.worker.onmessage = event => {
const { data } = event;
+4 -4
View File
@@ -17,10 +17,10 @@ function Card(props: { children: ReactNode }) {
<AriaClickable.Root
className={tw(
'group flex items-center gap-4 rounded-md border border-border-secondary p-4',
'data-[hovered]:bg-background-secondary',
'data-[pressed]:bg-fill-secondary-pressed',
'data-hovered:bg-background-secondary',
'data-pressed:bg-fill-secondary-pressed',
'outline-0 outline-border-focused',
'data-[focused]:outline-[2.5px]'
'data-focused:outline-[2.5px]'
)}
>
{props.children}
@@ -48,7 +48,7 @@ function CardSeeMoreLink(props: { onClick: () => void; children: ReactNode }) {
id={id}
className={tw(
'text-color-label-primary',
'group-data-[hovered]:underline'
'group-data-hovered:underline'
)}
>
{props.children}
+3 -3
View File
@@ -85,7 +85,7 @@ export namespace AriaClickable {
return (
<TriggerStateUpdateContext.Provider value={handleTriggerStateUpdate}>
<div
// eslint-disable-next-line better-tailwindcss/no-restricted-classes
// oxlint-disable-next-line better-tailwindcss/no-restricted-classes
className={tw('relative!', props.className)}
// For styling based on the HiddenTrigger state.
data-hovered={hovered ? true : null}
@@ -136,7 +136,7 @@ export namespace AriaClickable {
// [css-position-3: Painting Order and Stacking Contexts]: https://drafts.csswg.org/css-position-3/#stacking
// [css-flexbox-1: Flex Item Z-Ordering]: https://drafts.csswg.org/css-flexbox-1/#painting
//
// eslint-disable-next-line better-tailwindcss/no-restricted-classes
// oxlint-disable-next-line better-tailwindcss/no-restricted-classes
className={tw('contents *:relative *:z-20')}
>
{props.children}
@@ -160,7 +160,7 @@ export namespace AriaClickable {
*/
export const DeadArea: FC<DeadAreaProps> = memo(props => {
return (
// eslint-disable-next-line better-tailwindcss/no-restricted-classes
// oxlint-disable-next-line better-tailwindcss/no-restricted-classes
<div className={tw('relative! z-20!', props.className)}>
{props.children}
</div>
+1 -1
View File
@@ -185,7 +185,7 @@ export function Icons(): JSX.Element {
{size => (
<AxoAvatar.Root size={size}>
<AxoAvatar.Content label={null}>
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
{/* oxlint-disable-next-line typescript/no-non-null-assertion */}
<AxoAvatar.Icon symbol={icons[size % icons.length]!} />
</AxoAvatar.Content>
</AxoAvatar.Root>
+12 -13
View File
@@ -75,20 +75,20 @@ export namespace AxoAvatar {
};
const RingSizes: Record<Size, TailwindStyles | null> = {
20: tw('border-[1px] p-[1.5px]'),
24: tw('border-[1px] p-[1.5px]'),
20: tw('border p-[1.5px]'),
24: tw('border p-[1.5px]'),
28: tw('border-[1.5px] p-[2px]'),
30: tw('border-[1.5px] p-[2px]'),
32: tw('border-[1.5px] p-[2px]'),
36: tw('border-[1.5px] p-[2px]'),
40: tw('border-[1.5px] p-[2px]'),
48: tw('border-[2px] p-[3px]'),
52: tw('border-[2px] p-[3px]'),
64: tw('border-[2px] p-[3px]'),
48: tw('border-2 p-[3px]'),
52: tw('border-2 p-[3px]'),
64: tw('border-2 p-[3px]'),
72: tw('border-[2.5px] p-[3.5px]'),
80: tw('border-[2.5px] p-[3.5px]'),
96: tw('border-[3px] p-[4px]'),
216: tw('border-[4px] p-[6px]'),
216: tw('border-4 p-[6px]'),
};
export function _getAllSizes(): ReadonlyArray<Size> {
@@ -241,14 +241,13 @@ export namespace AxoAvatar {
}
return (
// eslint-disable-next-line jsx-a11y/alt-text
// oxlint-disable-next-line jsx-a11y/alt-text
<img
ref={ref}
src={props.src}
width={props.srcWidth}
height={props.srcHeight}
decoding="async"
// eslint-disable-next-line react/no-unknown-property
fetchPriority="low"
loading="lazy"
draggable={false}
@@ -288,7 +287,7 @@ export namespace AxoAvatar {
}, [preset]);
return (
// eslint-disable-next-line jsx-a11y/alt-text
// oxlint-disable-next-line jsx-a11y/alt-text
<img
src={src}
width={1024}
@@ -325,7 +324,7 @@ export namespace AxoAvatar {
className={tw(
'absolute inset-0 rounded-full',
'flex flex-col items-center-safe justify-center-safe gap-2',
// eslint-disable-next-line better-tailwindcss/no-restricted-classes
// oxlint-disable-next-line better-tailwindcss/no-restricted-classes
'bg-[#000]/20 text-[#fff] hover:bg-[#000]/40',
'outline-0 outline-border-focused focused:outline-[2.5px]'
)}
@@ -475,13 +474,13 @@ export namespace AxoAvatar {
const children = (
<>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
{/* oxlint-disable-next-line jsx-a11y/alt-text */}
<img
{...baseImageProps}
src={badge.light}
className={tw('size-full dark:hidden')}
/>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
{/* oxlint-disable-next-line jsx-a11y/alt-text */}
<img
{...baseImageProps}
src={badge.dark}
@@ -493,7 +492,7 @@ export namespace AxoAvatar {
const baseClassName = tw(
'absolute rounded-full',
// Proportionately sized & positioned based on the size of the avatar
'-end-[calc(2.75px-3%)] -bottom-[calc(6.25px-1%)] size-[calc(5px+37.5%)]'
'-inset-e-[calc(2.75px-3%)] -bottom-[calc(6.25px-1%)] size-[calc(5px+37.5%)]'
);
let result: ReactNode;
+2 -2
View File
@@ -58,8 +58,8 @@ export namespace AxoCheckbox {
'data-[state=checked]:bg-color-fill-primary',
'data-[state=checked]:text-label-primary-on-color',
'data-[state=checked]:pressed:bg-color-fill-primary-pressed',
'data-[disabled]:border-border-secondary',
'data-[state=checked]:data-[disabled]:text-label-disabled-on-color',
'data-disabled:border-border-secondary',
'data-[state=checked]:data-disabled:text-label-disabled-on-color',
'outline-0 outline-border-focused focused:outline-[2.5px]',
'overflow-hidden'
)}
+2 -5
View File
@@ -239,8 +239,7 @@ export namespace AxoContextMenu {
alignOffset={-6}
collisionPadding={6}
onCloseAutoFocus={props.onCloseAutoFocus}
// @ts-expect-error -- React/TS doesn't know about inert
inert={open ? undefined : 'true'}
inert={!open}
>
{props.children}
</ContextMenu.Content>
@@ -453,12 +452,10 @@ export namespace AxoContextMenu {
* -------------------------------------
*/
export type SeparatorProps = AxoBaseMenu.MenuSeparatorProps;
/**
* Used to visually separate items in the context menu.
*/
export const Separator: FC<SeparatorProps> = memo(() => {
export const Separator: FC = memo(() => {
return (
<ContextMenu.Separator className={AxoBaseMenu.menuSeparatorStyles} />
);
-1
View File
@@ -350,7 +350,6 @@ export function ExampleLanguageDialog(): React.JSX.Element {
<AxoDialog.ExperimentalSearch>
<input
type="search"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
placeholder="Search languages"
className={tw(
+1 -1
View File
@@ -334,7 +334,7 @@ export namespace AxoDialog {
// fit 1-2 words per line, push it up into its own row:
'min-w-[calc-size(fit-content,min(20ch,size))]',
// Allow it to fill its own row
'flex-grow',
'grow',
'type-body-large text-label-primary'
)}
>
+3 -6
View File
@@ -203,8 +203,7 @@ export namespace AxoDropdownMenu {
aria-labelledby={labelId}
aria-describedby={descriptionId}
onCloseAutoFocus={props.onCloseAutoFocus}
// @ts-expect-error -- React/TS doesn't know about inert
inert={open ? undefined : 'true'}
inert={!open}
>
{props.children}
</DropdownMenu.Content>
@@ -502,12 +501,10 @@ export namespace AxoDropdownMenu {
* --------------------------------------
*/
export type SeparatorProps = AxoBaseMenu.MenuSeparatorProps;
/**
* Used to visually separate items in the dropdown menu.
*/
export const Separator: FC<SeparatorProps> = memo(() => {
export const Separator: FC = memo(() => {
return (
<DropdownMenu.Separator className={AxoBaseMenu.menuSeparatorStyles} />
);
@@ -519,7 +516,7 @@ export namespace AxoDropdownMenu {
* Component: <AxoDropdownMenu.ContentSeparator>
*/
export const ContentSeparator: FC<SeparatorProps> = memo(() => {
export const ContentSeparator: FC = memo(() => {
return (
<DropdownMenu.Separator
className={AxoBaseMenu.menuContentSeparatorStyles}
+1 -1
View File
@@ -110,7 +110,7 @@ export namespace AxoMenuBuilder {
Label.displayName = `${Namespace}.Label`;
export const Separator: FC<AxoBaseMenu.MenuSeparatorProps> = memo(props => {
export const Separator: FC = memo(props => {
const renderer = useStrictContext(MenuBuilderContext);
if (renderer === 'AxoDropdownMenu') {
return <AxoDropdownMenu.Separator {...props} />;
+3 -7
View File
@@ -94,11 +94,7 @@ export namespace AxoRadioGroup {
* ------------------------------------
*/
export type IndicatorProps = Readonly<{
// ...
}>;
export const Indicator: FC<IndicatorProps> = memo(() => {
export const Indicator: FC = memo(() => {
const context = useStrictContext(ItemContext);
return (
<RadioGroup.Item
@@ -112,7 +108,7 @@ export namespace AxoRadioGroup {
'data-[state=unchecked]:pressed:bg-fill-primary-pressed',
'data-[state=checked]:bg-color-fill-primary',
'data-[state=checked]:pressed:bg-color-fill-primary-pressed',
'data-[disabled]:border-border-secondary',
'data-disabled:border-border-secondary',
'outline-0 outline-border-focused focused:outline-[2.5px]',
'overflow-hidden',
'forced-colors:data-[state=checked]:bg-[SelectedItem]'
@@ -123,7 +119,7 @@ export namespace AxoRadioGroup {
className={tw(
'size-[9px] rounded-full',
'data-[state=checked]:bg-label-primary-on-color',
'data-[state=checked]:data-[disabled]:bg-label-disabled-on-color',
'data-[state=checked]:data-disabled:bg-label-disabled-on-color',
'forced-colors:data-[state=checked]:bg-[SelectedItemText]'
)}
/>
+6 -6
View File
@@ -341,28 +341,28 @@ export namespace AxoScrollArea {
edgeYStyles,
edgeStartStyles,
'top-0',
'bg-gradient-to-b'
'bg-linear-to-b'
),
bottom: tw(
edgeStyles,
edgeYStyles,
edgeEndStyles,
'bottom-0',
'bg-gradient-to-t'
'bg-linear-to-t'
),
'inline-start': tw(
edgeStyles,
edgeXStyles,
edgeStartStyles,
'start-0',
'bg-gradient-to-r rtl:bg-gradient-to-l'
'inset-s-0',
'bg-linear-to-r rtl:bg-linear-to-l'
),
'inline-end': tw(
edgeStyles,
edgeXStyles,
edgeEndStyles,
'end-0',
'bg-gradient-to-l rtl:bg-gradient-to-r'
'inset-e-0',
'bg-linear-to-l rtl:bg-linear-to-r'
),
};
+6 -10
View File
@@ -134,7 +134,7 @@ export namespace AxoSelect {
},
'on-hover': {
chevronStyles: tw(
'absolute inset-y-0 end-0 w-9.5',
'absolute inset-y-0 inset-e-0 w-9.5',
'flex items-center justify-end pe-2',
'opacity-0 group-focus:opacity-100 group-data-[state=open]:opacity-100 group-hovered:opacity-100',
'transition-opacity duration-150'
@@ -146,10 +146,10 @@ export namespace AxoSelect {
'group-hovered:[--axo-select-trigger-mask-start:transparent]',
'group-focus:[--axo-select-trigger-mask-start:transparent]',
'group-data-[state=open]:[--axo-select-trigger-mask-start:transparent]',
'[mask-image:linear-gradient(to_left,var(--axo-select-trigger-mask-start)_19px,black_38px)]',
'rtl:[mask-image:linear-gradient(to_right,var(--axo-select-trigger-mask-start)_19px,black_38px)]',
'[mask-repeat:no-repeat]',
'[mask-position:right] rtl:[mask-position:left]',
'mask-[linear-gradient(to_left,var(--axo-select-trigger-mask-start)_19px,black_38px)]',
'rtl:mask-[linear-gradient(to_right,var(--axo-select-trigger-mask-start)_19px,black_38px)]',
'mask-no-repeat',
'mask-right rtl:mask-left',
'[transition-property:--axo-select-trigger-mask-start] duration-150'
),
},
@@ -408,14 +408,10 @@ export namespace AxoSelect {
* ---------------------------
*/
export type SeparatorProps = Readonly<{
// N/A
}>;
/**
* Used to visually separate items in the select.
*/
export const Separator: FC<SeparatorProps> = memo(() => {
export const Separator: FC = memo(() => {
return <Select.Separator className={AxoBaseMenu.selectSeperatorStyles} />;
});
+8 -8
View File
@@ -26,7 +26,7 @@ export namespace AxoSwitch {
'group relative z-0 flex h-[18px] w-8 items-center rounded-full',
'border border-border-secondary inset-shadow-on-color',
'bg-fill-secondary',
'data-[disabled]:bg-fill-primary',
'data-disabled:bg-fill-primary',
'pressed:bg-fill-secondary-pressed',
'outline-0 outline-border-focused focused:outline-[2.5px]',
'overflow-hidden'
@@ -34,21 +34,21 @@ export namespace AxoSwitch {
>
<span
className={tw(
'absolute top-0 bottom-0',
'absolute inset-y-0',
'w-5.5 rounded-s-full',
'group-data-[disabled]:w-7.5 group-data-[disabled]:rounded-full',
'group-data-disabled:w-7.5 group-data-disabled:rounded-full',
'opacity-0 group-data-[state=checked]:opacity-100',
'-translate-x-3.5 group-data-[state=checked]:translate-x-0 rtl:translate-x-3.5',
'bg-color-fill-primary group-pressed:bg-color-fill-primary-pressed',
'transition-all duration-200 ease-out-cubic',
'forced-colors:bg-[AccentColor]',
'forced-colors:group-data-[disabled]:bg-[GrayText]'
'forced-colors:group-data-disabled:bg-[GrayText]'
)}
/>
<span
className={tw(
'invisible forced-colors:visible',
'absolute start-0.5 z-0 text-[12px]',
'absolute inset-s-0.5 z-0 text-[12px]',
'forced-color-adjust-none',
'forced-colors:text-[AccentColorText]'
)}
@@ -58,16 +58,16 @@ export namespace AxoSwitch {
<Switch.Thumb
className={tw(
'z-10 block size-4 rounded-full',
// eslint-disable-next-line better-tailwindcss/no-restricted-classes
// oxlint-disable-next-line better-tailwindcss/no-restricted-classes
'shadow-[#000]/12',
'shadow-[0.5px_0_0.5px_0.5px,-0.5px_0_0.5px_0.5px]',
'bg-label-primary-on-color',
'data-[disabled]:bg-label-disabled-on-color',
'data-disabled:bg-label-disabled-on-color',
'transition-all duration-200 ease-out-cubic',
'data-[state=checked]:translate-x-3.5',
'rtl:data-[state=checked]:-translate-x-3.5',
'forced-colors:border',
'forced-colors:data-[disabled]:bg-[ButtonFace]'
'forced-colors:data-disabled:bg-[ButtonFace]'
)}
/>
</Switch.Root>
+1 -1
View File
@@ -108,7 +108,7 @@ export function All(): React.JSX.Element {
setInput(event.currentTarget.value);
}}
className={tw(
'w-full rounded bg-elevated-background-secondary p-3 type-body-medium'
'w-full rounded-sm bg-elevated-background-secondary p-3 type-body-medium'
)}
/>
</div>
+2 -2
View File
@@ -60,7 +60,7 @@ export namespace AxoTokens {
Number.isInteger(hash) && hash >= 0,
'Hash must be positive integer'
);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
return ALL_COLOR_NAMES[hash % ALL_COLOR_NAMES.length]!;
}
@@ -97,7 +97,7 @@ export namespace AxoTokens {
Number.isInteger(hash) && hash >= 0,
'Hash must be positive integer'
);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
return Gradients[hash % Gradients.length]!;
}
+12 -16
View File
@@ -41,13 +41,13 @@ export namespace AxoBaseMenu {
const navigableItemStyles = tw(
labeledItemStyles,
'curved-md type-body-medium',
'outline-0 data-[highlighted]:bg-fill-secondary-pressed',
'data-[disabled]:text-label-disabled',
'outline-0 data-highlighted:bg-fill-secondary-pressed',
'data-disabled:text-label-disabled',
'outline-0 outline-border-focused focused:outline-[2.5px]',
'forced-colors:text-[CanvasText]',
'forced-colors:data-[highlighted]:bg-[Highlight]',
'forced-colors:data-[highlighted]:text-[HighlightText]',
'forced-colors:data-[disabled]:text-[GrayText]',
'forced-colors:data-highlighted:bg-[Highlight]',
'forced-colors:data-highlighted:text-[HighlightText]',
'forced-colors:data-disabled:text-[GrayText]',
'forced-color-adjust-none'
);
@@ -165,7 +165,7 @@ export namespace AxoBaseMenu {
<span
dir="auto"
className={tw(
'ms-auto px-1 type-body-medium text-label-secondary forced-colors:text-[inherit]'
'ms-auto px-1 type-body-medium text-label-secondary forced-colors:text-inherit'
)}
>
{props.keyboardShortcut}
@@ -212,8 +212,8 @@ export namespace AxoBaseMenu {
export const menuContentStyles = tw(
baseContentStyles,
baseContentGridStyles,
'max-h-(--radix-popper-available-height) overflow-auto [scrollbar-width:none]',
'overflow-auto [scrollbar-width:none]'
'max-h-(--radix-popper-available-height) overflow-auto scrollbar-width-none',
'overflow-auto scrollbar-width-none'
);
export const selectContentStyles = tw(baseContentStyles);
@@ -338,10 +338,6 @@ export namespace AxoBaseMenu {
* ----------------------
*/
export type MenuSeparatorProps = Readonly<{
// N/A
}>;
const baseSeparatorStyles = tw('my-1 border-t-[0.5px] border-border-primary');
export const menuSeparatorStyles = tw(
@@ -375,9 +371,9 @@ export namespace AxoBaseMenu {
export const menuSubTriggerStyles = tw(
navigableItemStyles,
'data-[state=open]:not-data-[highlighted]:bg-fill-secondary',
'forced-colors:data-[state=open]:not-data-[highlighted]:bg-[Highlight]',
'forced-colors:data-[state=open]:not-data-[highlighted]:text-[HighlightText]'
'data-[state=open]:not-data-highlighted:bg-fill-secondary',
'forced-colors:data-[state=open]:not-data-highlighted:bg-[Highlight]',
'forced-colors:data-[state=open]:not-data-highlighted:text-[HighlightText]'
);
/**
@@ -391,7 +387,7 @@ export namespace AxoBaseMenu {
export const menuSubContentStyles = tw(
baseContentStyles,
'max-h-(--radix-popper-available-height) overflow-auto [scrollbar-width:none]',
'max-h-(--radix-popper-available-height) overflow-auto scrollbar-width-none',
baseContentGridStyles
);
}
+1 -1
View File
@@ -25,7 +25,7 @@ export const FlexWrapDetector = memo(function FlexWrapDetector(
<div className={tw('absolute -end-px size-px')} />
</div>
{/* 5. When not wrapped, this item should take priority when growing the items */}
<div className={tw('grow-[9999]')}>{props.children}</div>
<div className={tw('grow-9999')}>{props.children}</div>
</div>
);
});
+2 -2
View File
@@ -9,7 +9,7 @@ export function assert(condition: boolean, message?: string): asserts condition;
export function assert<T>(input: T, message?: string): NonNullable<T>;
export function assert<T>(input: T, message?: string): NonNullable<T> {
if (input === false || input == null) {
// eslint-disable-next-line no-debugger
// oxlint-disable-next-line no-debugger
debugger;
throw new AssertionError(message ?? `input is ${input}`);
}
@@ -17,7 +17,7 @@ export function assert<T>(input: T, message?: string): NonNullable<T> {
}
export function unreachable(_value: never): never {
// eslint-disable-next-line no-debugger
// oxlint-disable-next-line no-debugger
debugger;
throw new AssertionError('unreachable');
}
+9 -8
View File
@@ -1199,6 +1199,7 @@ export async function startApp(): Promise<void> {
} finally {
setupAppState();
drop(
// oxlint-disable-next-line promise/prefer-await-to-then
start().catch(error => {
log.error('start: threw an unexpected error', error);
})
@@ -1665,7 +1666,7 @@ export async function startApp(): Promise<void> {
while (afterAuthSocketConnectPromise?.promise) {
log.info(`${logId}: waiting for previous run to finish`);
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await afterAuthSocketConnectPromise.promise;
}
@@ -2126,7 +2127,7 @@ export async function startApp(): Promise<void> {
let lastRowId: number | null = 0;
while (lastRowId != null) {
const result =
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await DataWriter.dequeueOldestSyncTasks({
previousRowId: lastRowId,
incrementAttempts: false,
@@ -2140,9 +2141,9 @@ export async function startApp(): Promise<void> {
log.info(
`onEmpty/syncTasks: Queueing ${syncTasks.length} sync tasks for reattempt`
);
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await queueSyncTasks(syncTasks, DataWriter.removeSyncTaskById);
// eslint-disable-next-line no-await-in-loop
// oxlint-disable-next-line no-await-in-loop
await Promise.resolve(); // one tick
}
@@ -2448,7 +2449,7 @@ export async function startApp(): Promise<void> {
});
const { PROFILE_KEY_UPDATE } = Proto.DataMessage.Flags;
// eslint-disable-next-line no-bitwise
// oxlint-disable-next-line no-bitwise
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
if (isProfileUpdate) {
return handleMessageReceivedProfileUpdate({
@@ -2787,7 +2788,7 @@ export async function startApp(): Promise<void> {
}) {
// First set profileSharing = true for the conversation we sent to
const { id } = messageDescriptor;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
const conversation = window.ConversationController.get(id)!;
conversation.enableProfileSharing({
@@ -2797,7 +2798,7 @@ export async function startApp(): Promise<void> {
// Then we update our own profileKey if it's different from what we have
const ourId = window.ConversationController.getOurConversationId();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
const me = window.ConversationController.get(ourId)!;
const { profileKey } = data.message;
strictAssert(
@@ -3029,7 +3030,7 @@ export async function startApp(): Promise<void> {
});
const { PROFILE_KEY_UPDATE } = Proto.DataMessage.Flags;
// eslint-disable-next-line no-bitwise
// oxlint-disable-next-line no-bitwise
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
if (isProfileUpdate) {
return handleMessageSentProfileUpdate({
+5 -5
View File
@@ -1,10 +1,6 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-await-in-loop */
import { videoPixelFormatToEnum } from '@signalapp/ringrtc';
import type { VideoFrameSender, VideoFrameSource } from '@signalapp/ringrtc';
import type { RefObject } from 'react';
@@ -23,6 +19,7 @@ export class GumVideoCaptureOptions {
onEnded?: () => void;
}
// oxlint-disable-next-line typescript/consistent-type-definitions
interface GumTrackConstraints extends MediaTrackConstraints {
mandatory?: GumTrackConstraintSet;
}
@@ -46,6 +43,7 @@ export type SetLocalPreviewType = {
sizeCallback: SizeCallbackType | undefined;
};
// oxlint-disable-next-line max-classes-per-file
export class GumVideoCapturer {
private localPreview?: HTMLVideoElement;
private sizeCallback?: SizeCallbackType;
@@ -329,10 +327,11 @@ export class GumVideoCapturer {
}).readable.getReader();
const buffer = new Uint8Array(MAX_VIDEO_CAPTURE_BUFFER_SIZE);
this.spawnedSenderRunning = true;
// eslint-disable-next-line @typescript-eslint/no-floating-promises
// oxlint-disable-next-line typescript/no-floating-promises
(async () => {
try {
while (mediaStream === this.mediaStream) {
// oxlint-disable-next-line no-await-in-loop
const { done, value: frame } = await reader.read();
if (done) {
break;
@@ -352,6 +351,7 @@ export class GumVideoCapturer {
continue;
}
// oxlint-disable-next-line no-await-in-loop
await frame.copyTo(buffer);
if (sender !== this.sender) {
break;
@@ -24,7 +24,6 @@ export default {
},
} satisfies Meta<Props>;
// eslint-disable-next-line react/function-component-definition
const Template: StoryFn<Props> = args => {
return (
<AddUserToAnotherGroupModal
+1 -1
View File
@@ -68,7 +68,7 @@ export function AnimatedEmojiGalore({
<>
{springs.map((styles, index) => (
<animated.div
// eslint-disable-next-line react/no-array-index-key
// oxlint-disable-next-line react/no-array-index-key
key={index}
style={{
left: `${random(0, 100)}%`,
+2 -4
View File
@@ -84,7 +84,6 @@ const sizes = Object.values(AvatarSize).filter(
x => typeof x === 'number'
) as Array<AvatarSize>;
// eslint-disable-next-line react/function-component-definition
const Template: StoryFn<Props> = (args: Props) => {
return (
<>
@@ -95,7 +94,6 @@ const Template: StoryFn<Props> = (args: Props) => {
);
};
// eslint-disable-next-line react/function-component-definition
const TemplateSingle: StoryFn<Props> = (args: Props) => (
<Avatar {...args} size={AvatarSize.EIGHTY} />
);
@@ -104,12 +102,12 @@ export const Default = Template.bind({});
Default.args = createProps({
avatarUrl: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
Default.play = async (context: any) => {
const { args, canvasElement } = context;
const canvas = within(canvasElement);
const [avatar] = canvas.getAllByRole('button');
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// oxlint-disable-next-line typescript/no-non-null-assertion
await userEvent.click(avatar!);
await expect(args.onClick).toHaveBeenCalled();
};

Some files were not shown because too many files have changed in this diff Show More