Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
!.clang-format
!.editorconfig
!entry.ts
!eslint.config.js
!eslint.config.mjs
!ruff.toml
!stylesheet.xml
!svgo.config.js
Expand Down
20 changes: 6 additions & 14 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,24 +59,14 @@ echo 'source /black21-venv/bin/activate && black "$@"' > /usr/bin/black21
chmod +x /usr/bin/black21

# Install Node dependencies
#
# We stay on eslint-plugin-unicorn 56.0.1 because 57+ removed support for
# importing this plugin into the ESLint config as CommonJS. (We use CommonJS
# instead of ESM in the ESLint config mainly because the latter requires a new
# local package.json file that declares `"type": "module"`, which interferes
# with other tools like SVGO). I couldn't get the `deasync` hack to work - it
# just hung forever. TODO: Try updating this plugin after updating Node.js to
# v22+, which has experimental support for synchronously require()-ing ESM?
# https://github.com/sindresorhus/eslint-plugin-unicorn/releases/tag/v57.0.0
# https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
# https://nodejs.org/en/blog/announcements/v22-release-announce#support-requireing-synchronous-esm-graphs
# https://github.com/eslint/eslint/issues/13684#issuecomment-722949152
npm install -g \
@eslint/compat@2.0.5 \
@prettier/plugin-xml@3.4.2 \
eslint@9.39.2 \
eslint@10.2.0 \
eslint-plugin-jsdoc@62.9.0 \
eslint-plugin-perfectionist@5.8.0 \
eslint-plugin-sort-keys@2.3.5 \
eslint-plugin-unicorn@56.0.1 \
eslint-plugin-unicorn@64.0.0 \
prettier@3.8.3 \
svgo@4.0.1 \
typescript-eslint@8.58.2
Expand Down Expand Up @@ -133,3 +123,5 @@ COPY --from=entry /entry.js /entry
ENV COURSIER_CACHE=/tmp/coursier-cache
ENV COURSIER_JVM_CACHE=/tmp/coursier-jvm-cache
ENV NODE_PATH=/usr/local/lib/node_modules
# .mjs import resolution doesn't respect NODE_PATH
RUN ln -s /usr/local/lib/node_modules /node_modules
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This repo contains [pre-commit](https://pre-commit.com/) hooks for Duolingo deve
The main hook that runs several code formatters in parallel:

- [Prettier](https://github.com/prettier/prettier) v3.8.3 for CSS, HTML, JS, JSX, Markdown, Sass, TypeScript, XML, YAML
- [ESLint](https://eslint.org/) v9.39.2 for JS, TypeScript
- [ESLint](https://eslint.org/) v10.2.0 for JS, TypeScript
- [Ruff](https://docs.astral.sh/ruff/) v0.15.10 for Python 3
- [Black](https://github.com/psf/black) v21.12b0 for Python 2
- [autoflake](https://github.com/myint/autoflake) v1.7.8 for Python <!-- TODO: Upgrade to v2+, restrict to Python 2, and reenable Ruff rule F401 once our Python 3 repos that were converted from Python 2 no longer use type hint comments: https://github.com/PyCQA/autoflake/issues/222#issuecomment-1419089254 -->
Expand Down
2 changes: 1 addition & 1 deletion entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ const HOOKS: Record<HookName, Hook> = {
"eslint",
"--fix",
"--config",
"/eslint.config.js",
"/eslint.config.mjs",
...sources,
);
} catch {
Expand Down
67 changes: 49 additions & 18 deletions eslint.config.js → eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
const { defineConfig } = require("eslint/config");
const jsdoc = require("eslint-plugin-jsdoc");
const sortKeys = require("eslint-plugin-sort-keys");
const unicorn = require("eslint-plugin-unicorn");
const tseslint = require("typescript-eslint");
import { fixupPluginRules } from "@eslint/compat";
import { defineConfig } from "eslint/config";
import perfectionist from "eslint-plugin-perfectionist";
import { Alphabet } from "eslint-plugin-perfectionist/alphabet";
import jsdoc from "eslint-plugin-jsdoc";
import sortKeys from "eslint-plugin-sort-keys";
import unicorn from "eslint-plugin-unicorn";
import tseslint from "typescript-eslint";

const config = {
files: ["**/*.{js,jsx,mjs,ts,tsx}"],
Expand All @@ -19,7 +22,8 @@ const config = {
plugins: {
"@typescript-eslint": tseslint.plugin,
jsdoc,
"sort-keys": sortKeys,
perfectionist,
"sort-keys": fixupPluginRules(sortKeys),
unicorn,
},
//
Expand Down Expand Up @@ -90,7 +94,7 @@ const config = {
// repo-agnostic way. One compromise might be to use `import/order` and
// simply disable its regrouping feature in favor of whatever groups are
// found in the source code to be formatted, but no such option exists :/
"sort-imports": ["error", { ignoreDeclarationSort: true }],
// "sort-imports": ["error", { ignoreDeclarationSort: true }], // Replaced by perfectionist/sort-named-imports
"sort-vars": "error",
// "strict": "error",
// "unicode-bom": "error",
Expand Down Expand Up @@ -132,6 +136,19 @@ const config = {
// "jsdoc/tag-lines": "error",
// "jsdoc/text-escaping": "error",

// perfectionist rules. https://perfectionist.dev/rules
// "perfectionist/sort-enums" // Reordering can change numeric enum values
// "perfectionist/sort-heritage-clauses" // Not worth the churn when interfaces are involved
// "perfectionist/sort-imports" // TODO: Enable once grouping is more configurable
"perfectionist/sort-interfaces": "error",
// "perfectionist/sort-intersection-types" // Not worth the churn when interfaces are involved
"perfectionist/sort-named-exports": "error",
"perfectionist/sort-named-imports": "error",
"perfectionist/sort-object-types": "error",
// "perfectionist/sort-objects // Prefer sort-keys because it leaves computed properties alone
// "perfectionist/sort-switch-case" // TODO: Enable once it supports partitionByNewLine
// "perfectionist/sort-union-types" // Not worth the churn when interfaces are involved

// sort-keys rules. https://github.com/namnm/eslint-plugin-sort-keys
"sort-keys/sort-keys-fix": ["error", "asc", { natural: true }],

Expand All @@ -155,13 +172,7 @@ const config = {
],
// "@typescript-eslint/consistent-type-definitions": ["error", "interface"], // Can cause `Index signature for type 'string' is missing in type`
// "@typescript-eslint/consistent-type-imports": ["error", { disallowTypeAnnotations: false, prefer: "type-imports" }], // TODO: Enable once all our code is on TS 3.8+
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
accessibility: "explicit",
overrides: { accessors: "explicit", constructors: "explicit" },
},
],
// "@typescript-eslint/explicit-member-accessibility" // Buggy? Doesn't actually add `public` when absent
// "@typescript-eslint/method-signature-style": "error",
"@typescript-eslint/no-array-constructor": "error",
// "@typescript-eslint/no-dynamic-delete": "error",
Expand All @@ -182,17 +193,17 @@ const config = {
"unicorn/consistent-destructuring": "error",
"unicorn/consistent-empty-array-spread": "error",
// "unicorn/consistent-existence-index-check": "error",
// "unicorn/consistent-template-literal-escape": "error",
// "unicorn/custom-error-definition": "error",
// "unicorn/empty-brace-spaces": "error",
// "unicorn/escape-case": "error", // Implemented by Prettier
// "unicorn/explicit-length-check": "error",
// "unicorn/new-for-builtins": "error",
// "unicorn/no-array-for-each": "error", // Bug: fixer deletes comments
// "unicorn/no-array-method-this-argument": "error",
// "unicorn/no-array-push-push": "error", // Bug: fixer deletes comments
// "unicorn/no-await-expression-member": "error",
"unicorn/no-console-spaces": "error",
// "unicorn/no-for-loop": "error", // Bug: https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1802
"unicorn/no-for-loop": "error",
// "unicorn/no-hex-escape": "error",
// "unicorn/no-lonely-if": "error", // Bug: Moves comments around
"unicorn/no-negated-condition": "error",
Expand All @@ -207,12 +218,13 @@ const config = {
"unicorn/no-unnecessary-await": "error",
"unicorn/no-unreadable-array-destructuring": "error",
"unicorn/no-useless-fallback-in-spread": "error",
// "unicorn/no-useless-iterator-to-array": "error",
// "unicorn/no-useless-length-check": "error",
// "unicorn/no-useless-promise-resolve-reject": "error", // Conflicts with @typescript-eslint/no-throw-literal
"unicorn/no-useless-spread": "error",
// "unicorn/no-useless-undefined": "error", // Doesn't play well with TS
"unicorn/no-zero-fractions": "error",
"unicorn/number-literal-case": "error",
// "unicorn/number-literal-case": "error", // Implemented by Prettier
// "unicorn/numeric-separators-style": "error",
// "unicorn/prefer-add-event-listener": "error",
// "unicorn/prefer-array-find": "error", // Doesn't play well with TS, which types `filter(...)[0]` as non-undefined
Expand Down Expand Up @@ -249,6 +261,8 @@ const config = {
"unicorn/prefer-regexp-test": "error",
// "unicorn/prefer-set-has": "error",
"unicorn/prefer-set-size": "error",
// "unicorn/prefer-simple-condition-first": "error",
// "unicorn/prefer-single-call": "error", // Bug: fixer deletes comments
// "unicorn/prefer-spread": "error", // Bug: https://github.com/sindresorhus/eslint-plugin-unicorn/issues/2041
// "unicorn/prefer-string-raw": "error",
// "unicorn/prefer-string-replace-all": "error",
Expand All @@ -264,12 +278,29 @@ const config = {
// "unicorn/require-number-to-fixed-digits-argument": "error",
// "unicorn/string-content": "error",
// "unicorn/switch-case-braces": "error",
// "unicorn/switch-case-break-position": "error",
// "unicorn/template-indent": "error",
"unicorn/text-encoding-identifier-case": "error",
// "unicorn/throw-new-error": "error",

/* eslint-enable */
},
settings: {
perfectionist: {
// By default, perfectionist sorts lowercase before uppercase. We reverse
// that behavior here to match our existing pre-2026 convention, which is
// also arguably more sensible from a coding perspective because it
// prioritizes SCREAMING_SNAKE_CASE constants, similar to how such
// constants are typically defined first in a source file
alphabet: Alphabet.generateRecommendedAlphabet()
.placeAllWithCaseBeforeAllWithOtherCase("uppercase")
.getCharacters(),
ignoreCase: false,
order: "asc",
partitionByNewLine: true,
type: "custom",
},
},
};

module.exports = defineConfig([config]);
export default defineConfig([config]);
24 changes: 24 additions & 0 deletions test/after/hello.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,27 @@ try {
console.log("hi", `world${ex}`);
}
}

interface Serializable {
serialize(): string;
}
interface Printable {
print(): void;
}
class Report implements Serializable, Printable {
print(): void {}
serialize(): string {
return "";
}
}

interface Options {
Cache: boolean;
Format: string;
debug: boolean;
verbose: boolean;
}

type Point = { x: number; y: number };

export type { Combined, Level, Options, Point };
19 changes: 18 additions & 1 deletion test/before/hello.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { execSync , execFile } from "child_process" ;
import { writeFile, FSWatcher, readFile } from "fs" ;

Expand All @@ -25,3 +24,21 @@ try {
}catch( err) {
if (8.00 > foo!!) console.log("hi ", "world" + err);
}

interface Serializable { serialize(): string }
interface Printable { print(): void }
class Report implements Serializable, Printable {
print(): void {}
serialize(): string { return "" }
}

interface Options {
verbose: boolean;
Format: string;
debug: boolean;
Cache: boolean;
}

type Point = { y: number; x: number };

export type { Options, Combined, Level, Point };
Loading