Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mosaic-eslint-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/mosaic': patch
---

Use logical block spacing for feedback messages so their spacing follows the writing mode.
2 changes: 2 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import globals from 'globals';
import tseslint from 'typescript-eslint';

import { CUSTOM_BLOCK_TAGS, CUSTOM_MODIFIER_TAGS } from './.typedoc/custom-tags.mjs';
import mosaicQualityConfig from './packages/mosaic/eslint/quality-config.mjs';

const REPO_ROOT = import.meta.dirname;
const ECMA_VERSION = 2021,
Expand Down Expand Up @@ -589,6 +590,7 @@ export default tseslint.config([
},
},
...PACKAGE_IMPORT_RESOLVER_CONFIGS,
...mosaicQualityConfig,
{
name: 'packages/clerk-js',
files: ['packages/clerk-js/src/ui/**/*'],
Expand Down
219 changes: 219 additions & 0 deletions packages/mosaic/eslint/code-quality.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
function variableAt(sourceCode, node) {
let scope = sourceCode.getScope(node);
while (scope) {
const variable = scope.set.get(node.name);
if (variable) {
return variable;
}
scope = scope.upper;
}
return null;
}

function memberName(node) {
if (node.type !== 'MemberExpression') {
return null;
}
if (!node.computed && node.property.type === 'Identifier') {
return node.property.name;
}
if (node.computed && node.property.type === 'Literal') {
return node.property.value;
}
return null;
}

function accumulatorFor(context, node) {
const sourceCode = context.sourceCode;
for (const ancestor of sourceCode.getAncestors(node).reverse()) {
if (
ancestor.type !== 'ArrowFunctionExpression' &&
ancestor.type !== 'FunctionExpression' &&
ancestor.type !== 'FunctionDeclaration'
) {
continue;
}
const call = ancestor.parent;
if (call?.type !== 'CallExpression' || call.arguments[0] !== ancestor || memberName(call.callee) !== 'reduce') {
return null;
}
const parameter = ancestor.params[0];
if (parameter?.type !== 'Identifier') {
return null;
}
return variableAt(sourceCode, parameter);
}
return null;
}

function isAccumulator(context, node) {
const accumulator = node?.type === 'Identifier' ? accumulatorFor(context, node) : null;
return accumulator !== null && variableAt(context.sourceCode, node) === accumulator;
}

function isBuiltin(sourceCode, node) {
const variable = variableAt(sourceCode, node);
return variable === null || variable.defs.length === 0;
}

export const noAccumulatorCopy = {
meta: { type: 'suggestion', schema: [], messages: { copy: 'Avoid copying a reduce accumulator on each iteration.' } },
create(context) {
return {
SpreadElement(node) {
if (
(node.parent.type === 'ArrayExpression' || node.parent.type === 'ObjectExpression') &&
isAccumulator(context, node.argument)
) {
context.report({ node, messageId: 'copy' });
}
},
CallExpression(node) {
const name = memberName(node.callee);
if ((name === 'concat' || name === 'slice') && isAccumulator(context, node.callee.object)) {
context.report({ node, messageId: 'copy' });
}
if (
name === 'from' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'Array' &&
isBuiltin(context.sourceCode, node.callee.object) &&
isAccumulator(context, node.arguments[0])
) {
context.report({ node, messageId: 'copy' });
}
if (
name === 'assign' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'Object' &&
isBuiltin(context.sourceCode, node.callee.object) &&
node.arguments[0]?.type === 'ObjectExpression' &&
node.arguments[0].properties.length === 0 &&
isAccumulator(context, node.arguments[1])
) {
context.report({ node, messageId: 'copy' });
}
},
};
},
};

export const noChainedTypeAssertions = {
meta: { type: 'suggestion', schema: [], messages: { chain: 'Avoid chaining type assertions.' } },
create(context) {
function check(node) {
if (
node.parent?.expression === node &&
(node.parent.type === 'TSAsExpression' || node.parent.type === 'TSTypeAssertion')
) {
return;
}
if (node.expression.type === 'TSAsExpression' || node.expression.type === 'TSTypeAssertion') {
context.report({ node, messageId: 'chain' });
}
}
return { TSAsExpression: check, TSTypeAssertion: check };
},
};

function annotation(node) {
return node.typeAnnotation?.typeAnnotation ?? null;
}

function isWide(node, sourceCode) {
if (!node) {
return false;
}
if (node.type === 'TSUnknownKeyword' || node.type === 'TSObjectKeyword') {
return true;
}
return node.type === 'TSTypeReference' && sourceCode.getText(node).replaceAll(/\s/g, '') === 'Record<string,unknown>';
}

function functionBoundary(sourceCode, node) {
return (
sourceCode
.getAncestors(node)
.reverse()
.find(
ancestor =>
ancestor.type === 'FunctionDeclaration' ||
ancestor.type === 'FunctionExpression' ||
ancestor.type === 'ArrowFunctionExpression',
) ?? null
);
}

function typeKey(sourceCode, node) {
return sourceCode
.getTokens(node)
.map(token => token.value)
.join('');
}

export const noWidenThenAssert = {
meta: {
type: 'suggestion',
schema: [],
messages: { widen: 'Avoid widening a known type before asserting it back.' },
},
create(context) {
const sourceCode = context.sourceCode;
const known = new Map();
const widened = new Map();
function checkAssertion(node) {
if (node.expression.type !== 'Identifier') {
return;
}
const type = widened.get(variableAt(sourceCode, node.expression));
if (
type?.boundary === functionBoundary(sourceCode, node) &&
typeKey(sourceCode, node.typeAnnotation) === type.type
) {
context.report({ node, messageId: 'widen' });
}
}
return {
':function'(node) {
for (const param of node.params) {
if (param.type === 'Identifier' && annotation(param) && !isWide(annotation(param), sourceCode)) {
known.set(variableAt(sourceCode, param), { type: typeKey(sourceCode, annotation(param)), boundary: node });
}
}
},
VariableDeclarator(node) {
if (node.id.type !== 'Identifier' || node.parent.kind !== 'const') {
return;
}
const variable = variableAt(sourceCode, node.id);
const type = annotation(node.id);
if (type && !isWide(type, sourceCode)) {
known.set(variable, { type: typeKey(sourceCode, type), boundary: functionBoundary(sourceCode, node) });
}
const assertion =
node.init?.type === 'TSAsExpression' || node.init?.type === 'TSTypeAssertion' ? node.init : null;
const initializer = assertion ? assertion.expression : node.init;
const widenedType = type ?? assertion?.typeAnnotation;
if (initializer?.type !== 'Identifier') {
return;
}
const origin = variableAt(sourceCode, initializer);
if (isWide(widenedType, sourceCode) && known.get(origin)?.boundary === functionBoundary(sourceCode, node)) {
widened.set(variable, known.get(origin));
} else if (!widenedType && widened.has(origin)) {
widened.set(variable, widened.get(origin));
}
},
TSAsExpression: checkAssertion,
TSTypeAssertion: checkAssertion,
};
},
};

export default {
rules: {
'no-accumulator-copy': noAccumulatorCopy,
'no-chained-type-assertions': noChainedTypeAssertions,
'no-widen-then-assert': noWidenThenAssert,
},
};
67 changes: 67 additions & 0 deletions packages/mosaic/eslint/code-quality.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { RuleTester } from 'eslint';
import tseslint from 'typescript-eslint';
import { describe, test } from 'vitest';

import { noAccumulatorCopy, noChainedTypeAssertions, noWidenThenAssert } from './code-quality.mjs';

RuleTester.describe = describe;

RuleTester.it = test;

RuleTester.itOnly = test.only;

const js = new RuleTester();

const ts = new RuleTester({ languageOptions: { parser: tseslint.parser } });

js.run('no-accumulator-copy', noAccumulatorCopy, {
valid: [
'const result = values.reduce((acc, value) => { acc.push(value); return acc; }, []);',
'const copy = [...acc];',
'values.reduce((acc, value) => { consume(...acc); acc.push(value); return acc; }, []);',
'const result = values.reduce((acc, value) => { const nested = other.map(acc => [...acc]); acc.push(value); return acc; }, []);',
'const result = values.reduce((acc, value) => { const nested = other.map(() => [...value]); acc.push(value); return acc; }, []);',
'const result = values.reduce((acc, value) => { async function later() { return [...acc]; } acc.push(value); return acc; }, []);',
'function f(Object, Array) { return values.reduce((acc, value) => Object.assign({}, acc, value) || Array.from(acc), []); }',
],
invalid: [
{ code: 'values.reduce((acc, value) => [...acc, value], [])', errors: 1 },
{ code: 'values.reduce(function(acc, value) { return Object.assign({}, acc, value); }, {})', errors: 1 },
{ code: 'values.reduce((acc, value) => Array.from(acc), [])', errors: 1 },
{ code: 'values.reduce((acc, value) => acc.concat(value), [])', errors: 1 },
{ code: 'values.reduce((acc, value) => acc.slice(), [])', errors: 1 },
],
});

ts.run('no-chained-type-assertions', noChainedTypeAssertions, {
valid: ['const x = value as const;', 'const x = value as unknown;', 'const x = (value as const) satisfies Shape;'],
invalid: [
{ code: 'const x = value as unknown as Foo;', errors: 1 },
{ code: 'const x = ({ x: 1 } as const) as Foo;', errors: 1 },
{ code: 'const x = value as unknown as Foo as Bar;', errors: 1 },
{ code: 'const x = <Foo><unknown>value;', errors: 1 },
],
});

ts.run('no-widen-then-assert', noWidenThenAssert, {
valid: [
'const source: Foo = make(); const wide: unknown = source; const result = other as Foo;',
'const source: Foo = make(); let wide: unknown = source; const result = wide as Foo;',
'const source: Foo = make(); const wide: unknown = source; const result = (() => { const wide = other; return wide as Foo; })();',
'const source: Foo = make(); const wide: unknown = source; const result = wide as Bar;',
'const source: Foo = make(); function f() { const wide: unknown = source; return wide as Foo; }',
],
invalid: [
{ code: 'const source: Foo = make(); const wide: unknown = source; const result = wide as Foo;', errors: 1 },
{ code: 'const source: Foo = make(); const wide = source as unknown; const result = wide as Foo;', errors: 1 },
{ code: 'function f(source: Foo) { const wide = <unknown>source; return <Foo>wide; }', errors: 1 },
{
code: 'function f(source: Foo) { const wide: Record<string, unknown> = source; return wide as Foo; }',
errors: 1,
},
{
code: 'const source: Foo = make(); const wide: object = source; const alias = wide; const result = alias as Foo;',
errors: 1,
},
],
});
Loading
Loading