Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add eslint #2285

Merged
merged 25 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fcd83e4
add eslint tseslint and configure it so that it passes with warnings
Kakadus Sep 23, 2024
617f0b8
Prefer the safe `: unknown` for a `catch` callback variable
Kakadus Oct 21, 2024
04a96ea
The two values in this comparison do not have a shared enum type
Kakadus Oct 21, 2024
79a4332
autofix: Array type using 'Array<HTMLElement>' is forbidden. Use 'HTM…
Kakadus Oct 21, 2024
b2ff33e
use HTMLElement type in querySelector
Kakadus Oct 21, 2024
14fa5c0
autofix: Unnecessary 'else' after 'return'
Kakadus Oct 21, 2024
c519aee
Prefer using an optional chain expression instead, as it's more conci…
Kakadus Oct 21, 2024
5b4a56c
autofix: Unnecessary optional chain on a non-nullish value
Kakadus Oct 21, 2024
2fdee14
autofix: use const instead
Kakadus Oct 21, 2024
057c05e
autofix: Type string trivially inferred from a string literal, remove…
Kakadus Oct 21, 2024
3339bff
autofix+format: This assertion is unnecessary since it does not chang…
Kakadus Oct 21, 2024
843db52
autofix: The generic type arguments should be specified as part of th…
Kakadus Oct 21, 2024
ccbe81f
replace deprecated substr, add ignore to execCommand
Kakadus Oct 21, 2024
3a1875a
fix function parameters to Promise, don't use await on non-promise
Kakadus Oct 21, 2024
4c4a89d
await promises
Kakadus Oct 21, 2024
3cb156a
allow arrow-body-style
Kakadus Nov 25, 2024
d0190c3
Unsafe return of type `Map<any, any>` from function with return type …
Kakadus Oct 21, 2024
1e5bf52
type timeout as number
Kakadus Oct 21, 2024
d34563d
Move rules and make them a warning
Kakadus Oct 28, 2024
73feabf
add ESLint check
Kakadus Nov 4, 2024
6a759d8
force usage of arrow body as needed
Kakadus Nov 25, 2024
eab9a12
fixup! allow arrow-body-style
Kakadus Dec 2, 2024
13b9a5d
fixup! add eslint tseslint and configure it so that it passes with wa…
Kakadus Dec 2, 2024
d356aec
fixup! add ESLint check
Kakadus Dec 2, 2024
ce10eff
fixup! add eslint tseslint and configure it so that it passes with wa…
Kakadus Dec 2, 2024
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
7 changes: 7 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,17 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/setup_evap
with:
npm-ci: 'true'

- name: Run ruff
run: ruff check .
- name: Run pylint
run: pylint evap tools
- name: Run ESLint
run: |
cd evap/static/ts
npx eslint --quiet
Kakadus marked this conversation as resolved.
Show resolved Hide resolved

formatter:
runs-on: ubuntu-22.04
Expand Down
4 changes: 3 additions & 1 deletion evap/evaluation/management/commands/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ class Command(BaseCommand):
requires_migrations_checks = False

def handle(self, *args, **options):
self.stdout.write("Executing ruff .")
self.stdout.write("Executing ruff check .")
subprocess.run(["ruff", "check", "."], check=False) # nosec
self.stdout.write("Executing pylint evap")
subprocess.run(["pylint", "evap", "tools"], check=False) # nosec
self.stdout.write("Executing npx eslint --quiet")
subprocess.run(["npx", "eslint", "--quiet"], cwd="evap/static/ts", check=False) # nosec
3 changes: 2 additions & 1 deletion evap/evaluation/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,9 +453,10 @@ class TestLintCommand(TestCase):
@patch("subprocess.run")
def test_pylint_called(self, mock_subprocess_run: MagicMock):
management.call_command("lint", stdout=StringIO())
self.assertEqual(mock_subprocess_run.call_count, 2)
self.assertEqual(mock_subprocess_run.call_count, 3)
mock_subprocess_run.assert_any_call(["ruff", "check", "."], check=False)
mock_subprocess_run.assert_any_call(["pylint", "evap", "tools"], check=False)
mock_subprocess_run.assert_any_call(["npx", "eslint", "--quiet"], cwd="evap/static/ts", check=False)


class TestFormatCommand(TestCase):
Expand Down
54 changes: 54 additions & 0 deletions evap/static/ts/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// @ts-check

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
ignores: ["rendered/", "eslint.config.js"],
},
{
languageOptions: {
ecmaVersion: 2019,
parserOptions: {
project: ["tsconfig.json", "tsconfig.compile.json"],
},
sourceType: "module",
},
},
// ignore @typescript-eslint/no-misused-promises
Kakadus marked this conversation as resolved.
Show resolved Hide resolved
{
rules: {
"@typescript-eslint/restrict-template-expressions": ["error", { allowNumber: true }],
"@typescript-eslint/no-confusing-void-expression": ["error", { ignoreArrowShorthand: true }],
"no-else-return": "error",
"arrow-body-style": ["error", "as-needed"],
"@typescript-eslint/no-unused-vars": [
"warn",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unnecessary-type-parameters": "off",
"@typescript-eslint/no-unnecessary-condition": "warn",
"@typescript-eslint/no-misused-promises": "warn",
"@typescript-eslint/no-namespace": "warn",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unsafe-argument": "warn",
"@typescript-eslint/no-unsafe-assignment": "warn",
},
},
);
2 changes: 1 addition & 1 deletion evap/static/ts/src/base-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
document.addEventListener("shown.bs.modal", e => {
if (!e.target) return;
const modalEl = e.target as HTMLElement;
const autofocusEl = modalEl.querySelector("[autofocus]") as HTMLElement;
const autofocusEl = modalEl.querySelector<HTMLElement>("[autofocus]");
if (autofocusEl) {
autofocusEl.focus();
}
Expand Down
2 changes: 1 addition & 1 deletion evap/static/ts/src/confirmation-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class ConfirmationModal extends HTMLElement {
onDialogFormSubmit = (event: SubmitEvent) => {
event.preventDefault();

const isConfirm = event.submitter?.dataset?.eventType === "confirm";
const isConfirm = event.submitter?.dataset.eventType === "confirm";

if (isConfirm && this.internals.form && !this.internals.form.reportValidity()) {
return;
Expand Down
4 changes: 2 additions & 2 deletions evap/static/ts/src/contact_modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class ContactModalLogic {
private readonly successMessageModal: bootstrap.Modal;
private readonly actionButtonElement: HTMLButtonElement;
private readonly messageTextElement: HTMLInputElement;
private readonly showButtonElements: Array<HTMLElement>;
private readonly showButtonElements: HTMLElement[];
private readonly title: string;

// may be null if anonymous feedback is not enabled
Expand Down Expand Up @@ -39,7 +39,7 @@ export class ContactModalLogic {
try {
const response = await fetch("/contact", {
body: new URLSearchParams({
anonymous: String(this.anonymousRadioElement !== null && this.anonymousRadioElement.checked),
anonymous: String(this.anonymousRadioElement?.checked),
message,
title: this.title,
}),
Expand Down
3 changes: 2 additions & 1 deletion evap/static/ts/src/copy-to-clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ function copyToClipboard(text: string) {
document.body.appendChild(el);
const selected = selection.rangeCount > 0 ? selection.getRangeAt(0) : false;
el.select();
document.execCommand("copy");
// eslint-disable-next-line @typescript-eslint/no-deprecated
document.execCommand("copy"); // required by puppeteer tests
el.remove();
if (selected) {
selection.removeAllRanges();
Expand Down
2 changes: 1 addition & 1 deletion evap/static/ts/src/custom-success-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const overrideSuccessfulSubmit = (
assert(response.ok);
onSuccess({ body, response });
})
.catch(error => {
.catch((error: unknown) => {
console.error(error);
window.alert(window.gettext("The server is not responding."));
});
Expand Down
51 changes: 24 additions & 27 deletions evap/static/ts/src/datagrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ abstract class DataGrid {
protected container: HTMLElement;
private searchInput: HTMLInputElement;
protected rows: Row[] = [];
private delayTimer: any | null;
private delayTimer: number | undefined;
protected state: State;

protected constructor({ storageKey, head, container, searchInput }: DataGridParameters) {
Expand All @@ -59,7 +59,7 @@ abstract class DataGrid {
}

protected bindEvents() {
this.delayTimer = null;
this.delayTimer = undefined;
this.searchInput.addEventListener("input", () => {
clearTimeout(this.delayTimer);
this.delayTimer = setTimeout(() => {
Expand Down Expand Up @@ -87,7 +87,7 @@ abstract class DataGrid {
private static NUMBER_REGEX = /^[+-]?\d+(?:[.,]\d*)?$/;

private fetchRows(): Row[] {
let rows = [...this.container.children]
const rows = [...this.container.children]
.map(row => row as HTMLElement)
.map(row => {
const searchWords = this.findSearchableCells(row).flatMap(element =>
Expand Down Expand Up @@ -118,11 +118,11 @@ abstract class DataGrid {
protected abstract fetchRowFilterValues(row: HTMLElement): Map<string, string[]>;

private fetchRowOrderValues(row: HTMLElement): Map<string, string> {
let orderValues = new Map();
const orderValues = new Map<string, string>();
for (const column of this.sortableHeaders.keys()) {
const cell = row.querySelector<HTMLElement>(`[data-col=${column}]`)!;
if (cell.matches("[data-order]")) {
orderValues.set(column, cell.dataset.order);
orderValues.set(column, cell.dataset.order!);
} else {
orderValues.set(column, cell.innerHTML.trim());
}
Expand All @@ -138,20 +138,20 @@ abstract class DataGrid {
protected filterRows() {
const searchWords = DataGrid.searchWordsOf(this.state.search);
for (const row of this.rows) {
const isDisplayedBySearch = searchWords.every(searchWord => {
return row.searchWords.some(rowWord => rowWord.includes(searchWord));
});
const isDisplayedByFilters = [...this.state.equalityFilter].every(([name, filterValues]) => {
return filterValues.some(filterValue => {
return row.filterValues.get(name)?.some(rowValue => rowValue === filterValue);
});
});
const isDisplayedByRangeFilters = [...this.state.rangeFilter].every(([name, bound]) => {
return row.filterValues
const isDisplayedBySearch = searchWords.every(searchWord =>
row.searchWords.some(rowWord => rowWord.includes(searchWord)),
);
const isDisplayedByFilters = [...this.state.equalityFilter].every(([name, filterValues]) =>
filterValues.some(filterValue =>
row.filterValues.get(name)?.some(rowValue => rowValue === filterValue),
),
);
const isDisplayedByRangeFilters = [...this.state.rangeFilter].every(([name, bound]) =>
row.filterValues
.get(name)
?.map(rawValue => parseFloat(rawValue))
.some(rowValue => rowValue >= bound.low && rowValue <= bound.high);
});
.some(rowValue => rowValue >= bound.low && rowValue <= bound.high),
);
row.isDisplayed = isDisplayedBySearch && isDisplayedByFilters && isDisplayedByRangeFilters;
}
}
Expand Down Expand Up @@ -264,9 +264,8 @@ export class TableGrid extends DataGrid {
if (this.sortableHeaders.size > 0) {
const [firstColumn] = this.sortableHeaders.keys();
return [[firstColumn, "asc"]];
} else {
return [];
}
return [];
}
}

Expand All @@ -285,9 +284,9 @@ export class EvaluationGrid extends TableGrid {
public bindEvents() {
super.bindEvents();
this.filterButtons.forEach(button => {
const count = this.rows.filter(row => {
return row.filterValues.get("evaluationState")!.includes(button.dataset.filter!);
}).length;
const count = this.rows.filter(row =>
row.filterValues.get("evaluationState")!.includes(button.dataset.filter!),
).length;
button.append(EvaluationGrid.createBadgePill(count));

button.addEventListener("click", () => {
Expand Down Expand Up @@ -362,7 +361,7 @@ export class QuestionnaireGrid extends TableGrid {
body: new URLSearchParams(
this.rows.map((row, index) => [row.element.dataset.id!, index.toString()]),
),
}).catch(error => {
}).catch((error: unknown) => {
console.error(error);
window.alert(window.gettext("The server is not responding."));
});
Expand Down Expand Up @@ -477,7 +476,7 @@ export class ResultGrid extends DataGrid {
}

protected fetchRowFilterValues(row: HTMLElement): Map<string, string[]> {
let filterValues = new Map<string, string[]>();
const filterValues = new Map<string, string[]>();
for (const [name, { selector, checkboxes }] of this.filterCheckboxes.entries()) {
// To store filter values independent of the language, use the corresponding id from the checkbox
const values = [...row.querySelectorAll(selector)]
Expand Down Expand Up @@ -509,9 +508,7 @@ export class ResultGrid extends DataGrid {
checkboxes.forEach(checkbox => {
let isActive;
if (this.state.equalityFilter.has(name)) {
isActive = this.state.equalityFilter.get(name)!.some(filterValue => {
return filterValue === checkbox.value;
});
isActive = this.state.equalityFilter.get(name)!.some(filterValue => filterValue === checkbox.value);
} else {
isActive = false;
}
Expand Down
14 changes: 7 additions & 7 deletions evap/static/ts/src/quick-review-slider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export class QuickReviewSlider {
private readonly slider: HTMLElement;
private readonly reviewDecisionForm: HTMLFormElement;
private readonly flagForm: HTMLFormElement;
private readonly sliderItems: Array<HTMLElement>;
private answerSlides: Array<HTMLElement> = [];
private readonly sliderItems: HTMLElement[];
private answerSlides: HTMLElement[] = [];
private selectedSlideIndex = 0;

private readonly alertSlide: HTMLElement;
Expand All @@ -66,7 +66,7 @@ export class QuickReviewSlider {
private nextEvaluationIndex = 0;

private readonly startOverTriggers: { undecided: HTMLElement; all: HTMLElement };
private readonly slideTriggers: Array<HTMLElement>;
private readonly slideTriggers: HTMLElement[];
private readonly navigationButtons: { left: NavigationButtonWithCounters; right: NavigationButtonWithCounters };

constructor(
Expand Down Expand Up @@ -114,6 +114,7 @@ export class QuickReviewSlider {
assert(!this.isShowingEndslide(), "No answer slide is selected!");
return this.answerSlides[this.selectedSlideIndex];
}

public isShowingEndslide = () => this.selectedSlideIndex === this.answerSlides.length;

//
Expand Down Expand Up @@ -238,9 +239,8 @@ export class QuickReviewSlider {
}
};

private isWrongSubmit = (submitter: SubmitterElement) => {
return submitter.value === Action.MakePrivate && !("contribution" in this.selectedSlide.dataset);
};
private isWrongSubmit = (submitter: SubmitterElement) =>
(submitter.value as Action) === Action.MakePrivate && !("contribution" in this.selectedSlide.dataset);

private transitionHandler = (item: HTMLElement) => () => {
this.updateButtons();
Expand Down Expand Up @@ -421,7 +421,7 @@ export class QuickReviewSlider {
last.classList.add(`to-${reversed}`);
}

if (layer > 0) {
if (layer > Layer.Questionnaire) {
let activeInParentLayer;
if (nextActiveElement && !this.isShowingEndslide()) {
activeInParentLayer =
Expand Down
28 changes: 13 additions & 15 deletions evap/static/ts/src/student-vote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ function selectByNumberKey(row: HTMLElement, num: number) {
nextElement.click();
}

const studentForm = document.getElementById("student-vote-form") as HTMLElement;
const studentForm = document.getElementById("student-vote-form")!;
const selectables: NodeListOf<HTMLElement> = studentForm.querySelectorAll(".tab-selectable");
const rows = Array.from(studentForm.getElementsByClassName("tab-row")) as Array<HTMLElement>;
const rows = Array.from(studentForm.getElementsByClassName("tab-row")) as HTMLElement[];
const letterRegex = new RegExp("^[A-Za-zÄÖÜäöü.*+-]$");

// Sometimes we just want the browser to do its thing.
Expand Down Expand Up @@ -141,22 +141,20 @@ studentForm.addEventListener("keydown", (e: KeyboardEvent) => {
});

function findCorrectInputInRow(row: HTMLElement) {
const alreadySelectedElement: HTMLElement = row.querySelector(".tab-selectable:checked")!;
const alreadySelectedElement = row.querySelector<HTMLElement>(".tab-selectable:checked");

if (alreadySelectedElement) {
return alreadySelectedElement;
} else {
const possibleTargets: NodeListOf<HTMLElement> = row.querySelectorAll(".tab-selectable");
if (possibleTargets.length === 3) {
// Yes-No / No-Yes question, should focus first element
return possibleTargets[0];
} else {
// Everything else: The middle of all the answers excluding "no answer"
// This also handles all the single possibility cases
const index = Math.floor((possibleTargets.length - 1) / 2);
return possibleTargets[index];
}
}
const possibleTargets: NodeListOf<HTMLElement> = row.querySelectorAll(".tab-selectable");
if (possibleTargets.length === 3) {
// Yes-No / No-Yes question, should focus first element
return possibleTargets[0];
}
// Everything else: The middle of all the answers excluding "no answer"
// This also handles all the single possibility cases
const index = Math.floor((possibleTargets.length - 1) / 2);
return possibleTargets[index];
}

function fancyFocus(element: HTMLElement) {
Expand All @@ -171,7 +169,7 @@ document.querySelector("#btn-jump-unanswered-question")?.addEventListener("click

function scrollToFirstChoiceError() {
const firstErrorRow = document.querySelector(".row .choice-error");
const tabRow = firstErrorRow?.closest(".row")?.querySelector(".tab-row") as HTMLElement;
const tabRow = firstErrorRow?.closest(".row")?.querySelector<HTMLElement>(".tab-row");
if (tabRow) {
fancyFocus(findCorrectInputInRow(tabRow));
}
Expand Down
2 changes: 1 addition & 1 deletion evap/static/ts/src/text-answer-warnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function doesTextContainTriggerString(text: string, triggerStrings: string[]): b
function updateTextareaWarning(textarea: HTMLTextAreaElement, textAnswerWarnings: string[][]) {
const text = normalize(textarea.value);

let matchingWarnings = [];
const matchingWarnings = [];
if (isTextMeaningless(text)) {
matchingWarnings.push("meaningless");
}
Expand Down
Loading
Loading