mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
* refactor(locale): collapse inlined textLocale copies into the helper No behavior change: textLocale() is defined as isoTextLocale() ?? currentLocale(), which is exactly what these five sites had inlined. #9056 was based on master and so could not use the helper #9055 adds — it re-inlined the expression at focus-session, habit-tracker, worklog, scheduled-list and scheduled-date-group. Now that both have landed, collapse them so there is one canonical spelling instead of six. Caller specs mock textLocale() directly rather than the isoTextLocale/ currentLocale pair, mirroring what each SUT actually calls. * fix(locale): planner month label followed the browser locale, not the app's monthLabel passed no locale to toLocaleDateString, so the spelled-out month followed the *browser's* locale and ignored both the configured date locale and the UI language: a German browser rendered 'Juli 2026' in an English app. Same family as #8987 but reachable without the ISO option at all. Route it through textLocale(), which is the UI language under the ISO option and currentLocale() otherwise. The two existing specs computed their expected value with the same undefined locale, so they mirrored the bug and could never have caught it. They now pin a fixed app locale, deliberately not the runner's browser locale — otherwise they would pass either way. * test(lint): add require-text-locale rule and enforce it over src/app Guards the #8987 invariant that kept recurring: spelled-out weekday/month names must be formatted with textLocale(), never currentLocale() (the ISO option's 'sv' sentinel) and never the implicit browser locale. Three PRs chased this bug class site-by-site because currentLocale() is the obvious-looking default at every new call site. - Resolves `const locale = ...currentLocale()` through the scope chain: that is the shape the original bug had in plannedStartDateStr, so a rule matching only direct calls would have missed the very bug it exists to prevent. - Covers new Intl.DateTimeFormat() too — the same trap in constructor form, used at ~9 sites. Stays silent on clock times (hour/minute/dayPeriod), which must keep currentLocale() so the ISO 24h format survives. - Specs excluded: computing an expected string against an explicit locale is a legitimate test technique; the invariant is about what the product renders. - Documents its own blind spots (locale threaded through a parameter, reassigned variables, non-literal options) and pins them as valid cases, per the no-multi-entity-effect convention. Error severity is safe: textLocale() equals currentLocale() for every non-ISO option, so for spelled-out names it is never worse. Zero violations remain. * fix(lint): keep require-text-locale silent on clock-time formats The rule documented itself as staying silent on clock times, but `dayPeriod` sat in ALWAYS_SPELLED_OUT and only `.toLocaleTimeString()` was excluded — so `{ hour, minute, dayPeriod }` via `Intl.DateTimeFormat`/`toLocaleString` did fire, and its message told the reader to switch to textLocale(). Following that advice flips the ISO 24h clock to 12h: "13:05" -> "1:05 in the afternoon". The same holds for any mixed date+time options object: `{ weekday, hour }` goes from "onsdag 13:05" to "Wednesday 1:05 PM" — a Swedish name traded for a broken clock, the exact ISO regression this rule family exists to prevent. A format that mixes a spelled-out name with a clock has no single correct locale; it has to be split (names on textLocale(), clock on currentLocale(), as plannedStartDateStr does), which is more than a one-locale message can advise. So skip any options object containing `hour`, and say so. This costs a blind spot on `{ weekday, hour }` — cheaper than confidently wrong advice at `error` severity. No call site changes: `dayPeriod`/`era` have zero uses in src/, so this was latent. All four real bug shapes are still caught; src/app stays at zero violations. The two clock-time `valid` cases named dayPeriod in their comments but only ever tested `{ hour, minute }` in their code, which is how this slipped through — pin the actual shapes instead. * fix(lint): catch dateStyle in require-text-locale The rule missed `dateStyle` entirely, so the canonical #8987 shape walked straight past it: `toLocaleDateString(currentLocale(), { dateStyle: 'full' })` renders "onsdag 15 juli 2026" under the sentinel — a spelled-out weekday and month — without naming weekday or month at all. Zero call sites today, so this was latent, but guarding call sites that do not exist yet is the rule's whole job. `dateStyle` needs its own value set rather than month's: the two invert. `month: 'short'` is "Jul" (spelled out) but `dateStyle: 'short'` is "2026-07-15" (numeric), so reusing SPELLED_OUT_VALUES would have flagged dateStyle:'short' and pushed the reader to route ISO's YYYY-MM-DD through textLocale() — the mirror of the clock-time trap. Modelled as a per-field map so the inversion is stated where it can't be conflated, and pinned from both sides: 'short' as valid, 'full'/'medium'/'long' as invalid. Sabotage-verified — swapping in month's value set fails the spec. `timeStyle` joins `hour` as a clock-time field: `{ dateStyle, timeStyle }` is the mixed date+time case again ("onsdag 15 juli 2026 kl. 13:05" -> "Wednesday, July 15, 2026 at 1:05 PM"), and it carries no `hour` key for the existing guard to catch. Verified: 6/6 real bug shapes flagged, 0/6 false positives on correct usage, src/app still at zero violations.
306 lines
10 KiB
JavaScript
306 lines
10 KiB
JavaScript
// @ts-check
|
|
const tseslint = require('typescript-eslint');
|
|
const angular = require('angular-eslint');
|
|
const prettierRecommended = require('eslint-plugin-prettier/recommended');
|
|
const preferArrow = require('eslint-plugin-prefer-arrow');
|
|
const localRules = require('eslint-plugin-local-rules');
|
|
|
|
module.exports = tseslint.config(
|
|
// Global ignores
|
|
{
|
|
ignores: [
|
|
'app-builds/**/*',
|
|
'dist/**',
|
|
'node_modules/**/*',
|
|
'src/app/t.const.ts',
|
|
'src/assets/bundled-plugins/**/*',
|
|
'src/app/config/env.generated.ts',
|
|
'.tmp/**/*',
|
|
'packages/plugin-api/**/*',
|
|
'packages/plugin-dev/**/*',
|
|
'packages/shared-schema/**/*',
|
|
'packages/super-sync-server/**/*',
|
|
'packages/vite-plugin/**/*',
|
|
'packages/*/dist/**/*',
|
|
],
|
|
},
|
|
// TypeScript files
|
|
{
|
|
files: ['**/*.ts'],
|
|
extends: [
|
|
...tseslint.configs.recommended,
|
|
...angular.configs.tsRecommended,
|
|
prettierRecommended,
|
|
],
|
|
processor: angular.processInlineTemplates,
|
|
plugins: {
|
|
'prefer-arrow': preferArrow,
|
|
},
|
|
languageOptions: {
|
|
parserOptions: {
|
|
ecmaVersion: 2020,
|
|
sourceType: 'module',
|
|
},
|
|
},
|
|
rules: {
|
|
// Disabled rules
|
|
'@typescript-eslint/no-explicit-any': 'off',
|
|
'@typescript-eslint/ban-ts-comment': 'off',
|
|
'@typescript-eslint/no-empty-function': 'off',
|
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
'@angular-eslint/component-selector': 'off',
|
|
'@angular-eslint/no-input-rename': 'off',
|
|
'@typescript-eslint/no-inferrable-types': 'off',
|
|
'no-underscore-dangle': 'off',
|
|
'arrow-body-style': 'off',
|
|
'@typescript-eslint/member-ordering': 'off',
|
|
'import/order': 'off',
|
|
'arrow-parens': 'off',
|
|
'@typescript-eslint/explicit-member-accessibility': 'off',
|
|
|
|
// Enabled rules
|
|
'prettier/prettier': 'error',
|
|
'@typescript-eslint/no-unused-vars': [
|
|
'error',
|
|
{ args: 'none', caughtErrors: 'none' },
|
|
],
|
|
'@typescript-eslint/explicit-function-return-type': [
|
|
'error',
|
|
{
|
|
allowExpressions: true,
|
|
allowTypedFunctionExpressions: true,
|
|
allowHigherOrderFunctions: true,
|
|
allowDirectConstAssertionInArrowFunctions: true,
|
|
allowConciseArrowFunctionExpressionsStartingWithVoid: true,
|
|
},
|
|
],
|
|
'@typescript-eslint/naming-convention': [
|
|
'error',
|
|
{
|
|
selector: 'default',
|
|
format: ['camelCase', 'snake_case', 'UPPER_CASE', 'PascalCase'],
|
|
leadingUnderscore: 'allowSingleOrDouble',
|
|
trailingUnderscore: 'allow',
|
|
filter: { regex: '(should)|@tags', match: false },
|
|
},
|
|
{
|
|
selector: 'variable',
|
|
format: ['camelCase', 'snake_case', 'UPPER_CASE', 'PascalCase'],
|
|
leadingUnderscore: 'allowSingleOrDouble',
|
|
trailingUnderscore: 'allow',
|
|
},
|
|
{ selector: 'enum', format: ['PascalCase', 'UPPER_CASE'] },
|
|
{ selector: 'typeLike', format: ['PascalCase'] },
|
|
],
|
|
'prefer-const': 'error',
|
|
'@typescript-eslint/no-unused-expressions': 'error',
|
|
'@typescript-eslint/no-empty-object-type': 'error',
|
|
'max-len': [
|
|
'error',
|
|
{
|
|
ignorePattern: '^import \\{.+;$',
|
|
ignoreRegExpLiterals: true,
|
|
ignoreStrings: true,
|
|
ignoreUrls: true,
|
|
code: 150,
|
|
},
|
|
],
|
|
'id-blacklist': 'error',
|
|
// @typescript-eslint/member-delimiter-style removed in v8 - Prettier handles this
|
|
'no-shadow': 'off',
|
|
'@typescript-eslint/no-shadow': 'error',
|
|
'comma-dangle': ['error', 'always-multiline'],
|
|
'no-mixed-operators': 'error',
|
|
'prefer-arrow/prefer-arrow-functions': 'error',
|
|
'@angular-eslint/directive-selector': [
|
|
'error',
|
|
{ type: 'attribute', prefix: '', style: 'camelCase' },
|
|
],
|
|
// @typescript-eslint/ban-types replaced by specific rules in v8
|
|
'@typescript-eslint/no-unsafe-function-type': 'error',
|
|
'@typescript-eslint/no-wrapper-object-types': 'error',
|
|
},
|
|
},
|
|
{
|
|
files: ['packages/sync-core/**/*.ts'],
|
|
rules: {
|
|
'no-restricted-imports': [
|
|
'error',
|
|
{
|
|
paths: [
|
|
{
|
|
name: '@sp/shared-schema',
|
|
message:
|
|
'@sp/sync-core must stay domain-agnostic; shared-schema is SP-specific.',
|
|
},
|
|
],
|
|
patterns: [
|
|
{
|
|
group: [
|
|
'@angular/*',
|
|
'@ngrx/*',
|
|
'@sp/shared-schema/*',
|
|
'../shared-schema/*',
|
|
'../shared-schema/**',
|
|
'../../shared-schema/*',
|
|
'../../shared-schema/**',
|
|
'**/shared-schema/*',
|
|
'**/shared-schema/**',
|
|
'src/app/*',
|
|
'src/app/**',
|
|
'**/src/app/*',
|
|
'**/src/app/**',
|
|
],
|
|
message:
|
|
'@sp/sync-core must not import Angular, NgRx, app code, or SP-specific schema packages.',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
'no-restricted-syntax': [
|
|
'error',
|
|
{
|
|
selector: 'ImportExpression',
|
|
message:
|
|
'@sp/sync-core must not use dynamic imports; they bypass package-boundary checks.',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
files: ['packages/sync-providers/**/*.ts'],
|
|
rules: {
|
|
'no-restricted-imports': [
|
|
'error',
|
|
{
|
|
paths: [
|
|
{
|
|
name: '@sp/shared-schema',
|
|
message: '@sp/sync-providers must not import SP-specific schema packages.',
|
|
},
|
|
],
|
|
patterns: [
|
|
{
|
|
group: [
|
|
'@angular/*',
|
|
'@ngrx/*',
|
|
'@sp/shared-schema/*',
|
|
'@sp/sync-core/*',
|
|
'**/shared-schema/*',
|
|
'**/shared-schema/**',
|
|
'**/sync-core/*',
|
|
'**/sync-core/**',
|
|
'src/app/*',
|
|
'src/app/**',
|
|
'**/src/app/*',
|
|
'**/src/app/**',
|
|
],
|
|
message:
|
|
'@sp/sync-providers must use only public @sp/sync-core exports and must not import Angular, NgRx, app code, or SP-specific schema packages.',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
'no-restricted-syntax': [
|
|
'error',
|
|
{
|
|
selector: 'ImportExpression',
|
|
message:
|
|
'@sp/sync-providers must not use dynamic imports; they bypass package-boundary checks.',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
// NgRx effects files - require hydration guards on selector-based effects
|
|
{
|
|
files: ['**/*.effects.ts'],
|
|
plugins: {
|
|
'local-rules': localRules,
|
|
},
|
|
rules: {
|
|
'local-rules/require-hydration-guard': 'error',
|
|
'local-rules/require-entity-registry': 'warn',
|
|
'local-rules/no-actions-in-effects': 'error',
|
|
'local-rules/no-multi-entity-effect': 'warn',
|
|
},
|
|
},
|
|
// Spelled-out weekday/month names must be formatted with textLocale(), not
|
|
// currentLocale() (the ISO option's `sv` sentinel) or the implicit browser
|
|
// locale — see #8987, which recurred across three PRs. Specs are excluded:
|
|
// computing an expected string against an explicit locale is a legitimate
|
|
// test technique, and the invariant is about what the product renders.
|
|
{
|
|
files: ['src/app/**/*.ts'],
|
|
ignores: ['**/*.spec.ts'],
|
|
plugins: {
|
|
'local-rules': localRules,
|
|
},
|
|
rules: {
|
|
'local-rules/require-text-locale': 'error',
|
|
},
|
|
},
|
|
// Op-log persistence: inside an adapter.transaction() callback only the tx
|
|
// handle may be used — adapter methods enqueue behind the transaction's own
|
|
// FIFO queue slot on the SQLite backend and deadlock (see
|
|
// SqliteOpLogAdapter._serialize()).
|
|
{
|
|
files: ['src/app/op-log/**/*.ts'],
|
|
plugins: {
|
|
'local-rules': localRules,
|
|
},
|
|
rules: {
|
|
'local-rules/no-adapter-in-tx': 'error',
|
|
},
|
|
},
|
|
// App code must route logging through Log/SyncLog/OpLog/... helpers.
|
|
// Direct console.* calls bypass the exportable log history users attach
|
|
// to bug reports. The Log implementation itself, tests, and benchmarks
|
|
// (which intentionally dump timing numbers to stdout) are exempt.
|
|
{
|
|
files: ['src/app/**/*.ts'],
|
|
ignores: ['src/app/**/*.spec.ts', 'src/app/**/*.benchmark.ts', 'src/app/core/log.ts'],
|
|
rules: {
|
|
'no-console': 'error',
|
|
},
|
|
},
|
|
// Service size cap (AGENTS.md → Project rules): no service may exceed 1200
|
|
// lines. 'error' so a new service crossing the cap fails CI on the PR that
|
|
// introduces it — 'warn' would be inert, since `ng lint` defaults to
|
|
// maxWarnings: -1 and never fails a build on warnings. Spec files end in
|
|
// `.service.spec.ts`, so they are not matched by this glob and are exempt.
|
|
{
|
|
files: ['**/*.service.ts'],
|
|
rules: {
|
|
'max-lines': ['error', { max: 1200 }],
|
|
},
|
|
},
|
|
// Grandfathered offenders: services already over the cap, downgraded to a
|
|
// (non-failing) warning so they don't red-CI while they are split down. They
|
|
// still warn at their real size, so the debt stays visible in lint output.
|
|
// This list may only ever SHRINK — a new entry means the cap was bypassed.
|
|
// Delete the block once every file below is under 1200 lines.
|
|
{
|
|
files: [
|
|
'src/app/op-log/sync/conflict-resolution.service.ts',
|
|
'src/app/op-log/sync-providers/file-based/file-based-sync-adapter.service.ts',
|
|
'src/app/op-log/persistence/operation-log-store.service.ts',
|
|
'src/app/op-log/sync/operation-log-sync.service.ts',
|
|
'src/app/plugins/plugin-bridge.service.ts',
|
|
'src/app/plugins/plugin.service.ts',
|
|
'src/app/imex/sync/sync-wrapper.service.ts',
|
|
'src/app/features/tasks/task.service.ts',
|
|
],
|
|
rules: {
|
|
'max-lines': ['warn', { max: 1200 }],
|
|
},
|
|
},
|
|
// HTML files
|
|
{
|
|
files: ['**/*.html'],
|
|
extends: [...angular.configs.templateRecommended, prettierRecommended],
|
|
rules: {
|
|
'@angular-eslint/template/no-negated-async': 'off',
|
|
'prettier/prettier': 'error',
|
|
},
|
|
},
|
|
);
|