chore(encoding): eslint/tsconfig migration

Migrate to eslint flat config (qr-code.ts override for the mask-penalty
sliding-window) and composite tsconfig.
This commit is contained in:
2026-06-07 16:29:27 +07:00
parent a7e668ced8
commit da8d137be4
13 changed files with 103 additions and 41 deletions
+13
View File
@@ -0,0 +1,13 @@
import { base, compose, imports, stylistic, typescript } from '@robonen/eslint';
export default compose(base, typescript, imports, stylistic, {
name: 'encoding/overrides',
files: ['src/qr/qr-code.ts'],
rules: {
'@stylistic/max-statements-per-line': 'off',
'@stylistic/no-mixed-operators': 'off',
/* Uniform sliding-window register shift (h6 = h5; h5 = h4; …) where the
oldest register's seed/last write is intentionally dead — keep symmetry. */
'no-useless-assignment': 'off',
},
});
-14
View File
@@ -1,14 +0,0 @@
import { defineConfig } from 'oxlint';
import { compose, base, typescript, imports, stylistic } from '@robonen/oxlint';
export default defineConfig(compose(base, typescript, imports, stylistic, {
overrides: [
{
files: ['src/qr/qr-code.ts'],
rules: {
'@stylistic/max-statements-per-line': 'off',
'@stylistic/no-mixed-operators': 'off',
},
},
],
}));
+4 -5
View File
@@ -34,19 +34,18 @@
}
},
"scripts": {
"lint:check": "oxlint -c oxlint.config.ts",
"lint:fix": "oxlint -c oxlint.config.ts --fix",
"lint:check": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest run",
"dev": "vitest dev",
"bench": "vitest bench",
"build": "tsdown"
},
"devDependencies": {
"@robonen/oxlint": "workspace:*",
"@robonen/eslint": "workspace:*",
"@robonen/tsconfig": "workspace:*",
"@robonen/tsdown": "workspace:*",
"@stylistic/eslint-plugin": "catalog:",
"oxlint": "catalog:",
"eslint": "catalog:",
"tsdown": "catalog:"
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { bench, describe } from 'vitest';
import { encodeBinary, encodeSegments, encodeText, makeSegments, LOW, EccMap } from '..';
import { EccMap, LOW, encodeBinary, encodeSegments, encodeText, makeSegments } from '..';
/* -- Test data -- */
+37 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { encodeText, encodeBinary, encodeSegments, makeSegments, isNumeric, isAlphanumeric, QrCode, QrCodeDataType, EccMap, LOW, MEDIUM, QUARTILE, HIGH } from '..';
import { EccMap, HIGH, LOW, MEDIUM, QUARTILE, QrCode, QrCodeDataType, encodeBinary, encodeSegments, encodeText, isAlphanumeric, isNumeric, makeSegments } from '..';
describe('isNumeric', () => {
it('accepts pure digit strings', () => {
@@ -255,3 +255,39 @@ describe('getType semantics', () => {
expect(qr.getType(8, 6)).toBe(QrCodeDataType.Timing);
});
});
describe('automatic mask selection (regression)', () => {
// The mask is chosen by minimizing getPenaltyScore() over all 8 patterns.
// These version/mask pairs were verified against the canonical Project Nayuki
// penalty algorithm. A regression in the finder-pattern (PENALTY_N3) rolling
// window — e.g. dropping a history slot during the shift — silently selects a
// different, non-spec-optimal mask, which these fixtures catch.
it.each([
['HELLO WORLD', QUARTILE, 1, 0],
['HELLO WORLD', MEDIUM, 1, 0],
['https://example.com', LOW, 2, 0],
['12345678901234567890', LOW, 1, 4],
['Hello, World!', MEDIUM, 1, 3],
['a'.repeat(200), LOW, 9, 1],
] as const)('locks version/mask for %j', (text, ecc, version, mask) => {
const qr = encodeText(text, ecc);
expect(qr.version).toBe(version);
expect(qr.mask).toBe(mask);
});
it('produces a stable full-grid fingerprint for HELLO WORLD / QUARTILE', () => {
const qr = encodeText('HELLO WORLD', QUARTILE);
let dark = 0;
let hash = 0;
for (let y = 0; y < qr.size; y++) {
for (let x = 0; x < qr.size; x++) {
const bit = qr.getModule(x, y) ? 1 : 0;
dark += bit;
hash = (hash * 31 + bit) >>> 0;
}
}
expect(qr.size).toBe(21);
expect(dark).toBe(218);
expect(hash).toBe(1_118_257_212);
});
});
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { QrSegment, makeNumeric, makeAlphanumeric, makeBytes } from '../segment';
import { QrSegment, makeAlphanumeric, makeBytes, makeNumeric } from '../segment';
import { MODE_ALPHANUMERIC, MODE_BYTE, MODE_NUMERIC } from '../constants';
describe('QrSegment', () => {
+2 -2
View File
@@ -21,7 +21,7 @@ export function encodeText(text: string, ecl: QrCodeEcc): QrCode {
* This function always encodes using the binary segment mode, not any text mode.
* The maximum number of bytes allowed is 2953.
*/
export function encodeBinary(data: Readonly<number[]>, ecl: QrCodeEcc): QrCode {
export function encodeBinary(data: readonly number[], ecl: QrCodeEcc): QrCode {
const seg = makeBytes(data);
return encodeSegments([seg], ecl);
}
@@ -32,7 +32,7 @@ export function encodeBinary(data: Readonly<number[]>, ecl: QrCodeEcc): QrCode {
* This is a mid-level API; the high-level API is encodeText() and encodeBinary().
*/
export function encodeSegments(
segs: Readonly<QrSegment[]>,
segs: readonly QrSegment[],
ecl: QrCodeEcc,
minVersion = 1,
maxVersion = 40,
+21 -15
View File
@@ -38,7 +38,7 @@ export class QrCode {
public readonly version: number,
/** The error correction level used in this QR Code. */
public readonly ecc: QrCodeEcc,
dataCodewords: Readonly<number[]>,
dataCodewords: readonly number[],
msk: number,
) {
if (version < MIN_VERSION || version > MAX_VERSION)
@@ -202,7 +202,7 @@ export class QrCode {
/* -- Private helper methods for constructor: Codewords and masking -- */
private addEccAndInterleave(data: Readonly<number[]>): number[] {
private addEccAndInterleave(data: readonly number[]): number[] {
const ver = this.version;
const ecl = this.ecc;
if (data.length !== getNumDataCodewords(ver, ecl))
@@ -239,7 +239,7 @@ export class QrCode {
return result;
}
private drawCodewords(data: Readonly<number[]>): void {
private drawCodewords(data: readonly number[]): void {
if (data.length !== ((getNumRawDataModules(this.version) / 8) | 0))
throw new RangeError('Invalid argument');
@@ -317,8 +317,9 @@ export class QrCode {
result++;
}
else {
if (h0 === 0) runX += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = runX; h0 = runX;
let v = runX;
if (h0 === 0) v += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
if (runColor === 0) {
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
if (core && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
@@ -331,13 +332,15 @@ export class QrCode {
{
let currentRunLength = runX;
if (runColor === 1) {
if (h0 === 0) currentRunLength += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = currentRunLength;
let v = currentRunLength;
if (h0 === 0) v += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
currentRunLength = 0;
}
currentRunLength += size;
if (h0 === 0) currentRunLength += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = currentRunLength;
let v = currentRunLength;
if (h0 === 0) v += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
if (core && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
if (core && h6 >= h1 * 4 && h0 >= h1) result += PENALTY_N3;
@@ -359,8 +362,9 @@ export class QrCode {
result++;
}
else {
if (h0 === 0) runY += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = runY; h0 = runY;
let v = runY;
if (h0 === 0) v += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
if (runColor === 0) {
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
if (core && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
@@ -373,13 +377,15 @@ export class QrCode {
{
let currentRunLength = runY;
if (runColor === 1) {
if (h0 === 0) currentRunLength += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = currentRunLength;
let v = currentRunLength;
if (h0 === 0) v += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
currentRunLength = 0;
}
currentRunLength += size;
if (h0 === 0) currentRunLength += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = currentRunLength;
let v = currentRunLength;
if (h0 === 0) v += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
if (core && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
if (core && h6 >= h1 * 4 && h0 >= h1) result += PENALTY_N3;
+1 -1
View File
@@ -67,7 +67,7 @@ export function getNumDataCodewords(ver: number, ecl: QrCodeEcc): number {
* Calculates and returns the number of bits needed to encode the given segments at the given version.
* The result is infinity if a segment has too many characters to fit its length field.
*/
export function getTotalBits(segs: Readonly<QrSegment[]>, version: number): number {
export function getTotalBits(segs: readonly QrSegment[], version: number): number {
let result = 0;
for (const seg of segs) {
const ccbits = numCharCountBits(seg.mode, version);
+5 -1
View File
@@ -1,3 +1,7 @@
{
"extends": "@robonen/tsconfig/tsconfig.json"
"files": [],
"references": [
{ "path": "./tsconfig.src.json" },
{ "path": "./tsconfig.node.json" }
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@robonen/tsconfig/tsconfig.node.json",
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
},
"include": ["*.config.ts"]
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "@robonen/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"composite": true,
"types": [],
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.src.tsbuildinfo"
},
"include": ["src/**/*.ts"]
}
+1
View File
@@ -3,5 +3,6 @@ import { sharedConfig } from '@robonen/tsdown';
export default defineConfig({
...sharedConfig,
tsconfig: './tsconfig.src.json',
entry: ['src/index.ts'],
});