Repository: refined-github/refined-github
Pull request #7655 (2024-08-03 17:13:34)

Meta: Validate feature renames
- Part of https://github.com/refined-github/refined-github/issues/7132

It indeed caught a few issues 😇


Patch:
diff --git a/.github/ISSUE_TEMPLATE/1_bug_report.yml b/.github/ISSUE_TEMPLATE/1_bug_report.yml
index 57a25418..1e0eabb5 100644
--- a/.github/ISSUE_TEMPLATE/1_bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/1_bug_report.yml
@@ -44,3 +44,10 @@ body:
       label: Browser(s) used
     validations:
       required: true
+  - id: token
+    type: checkboxes
+    attributes:
+      label: Token ⚠️
+      options:
+        - label: I set a token in the options page. I understand that some features require a token to work.
+          required: true
diff --git a/source/feature-renames.json b/source/feature-renames.json
new file mode 100644
index 00000000..fc53f4e8
--- /dev/null
+++ b/source/feature-renames.json
@@ -0,0 +1,39 @@
+{
+	"separate-draft-pr-button": "one-click-pr-or-gist",
+	"prevent-pr-commit-link-loss": "prevent-link-loss",
+	"remove-projects-tab": "clean-repo-tabs",
+	"remove-unused-repo-tabs": "clean-repo-tabs",
+	"more-dropdown": "clean-repo-tabs",
+	"remove-diff-signs": "hide-diff-signs",
+	"remove-label-faster": "quick-label-removal",
+	"edit-files-faster": "quick-file-edit",
+	"edit-comments-faster": "quick-comment-edit",
+	"delete-review-comments-faster": "quick-review-comment-deletion",
+	"hide-comments-faster": "quick-comment-hiding",
+	"faster-reviews": "quick-review",
+	"faster-pr-diff-options": "one-click-diff-options",
+	"hide-useless-comments": "hide-low-quality-comments",
+	"hide-useless-newsfeed-events": "hide-newsfeed-noise",
+	"hide-noisy-newsfeed-events": "hide-newsfeed-noise",
+	"no-useless-split-diff-view": "no-unnecessary-split-diff-view",
+	"unwrap-useless-dropdowns": "unwrap-unnecessary-dropdowns",
+	"tag-changelog-link": "tag-changes-link",
+	"navigate-pages-with-arrow-keys": "pagination-hotkey",
+	"list-pr-for-branch": "list-prs-for-branch",
+	"quick-label-hiding": "quick-label-removal",
+	"next-scheduled-github-action": "github-actions-indicators",
+	"raw-file-link": "more-file-links",
+	"conversation-filters": "more-conversation-filters",
+	"quick-pr-diff-options": "one-click-diff-options",
+	"quick-review-buttons": "one-click-review-submission",
+	"pull-request-hotkey": "pull-request-hotkeys",
+	"first-published-tag-for-merged-pr": "closing-remarks",
+	"scheduled-and-manual-workflow-indicators": "github-actions-indicators",
+	"useful-forks": "archive-forks-link",
+	"set-default-repositories-type-to-sources": "hide-user-forks",
+	"highlight-deleted-and-added-files-in-diffs": "new-or-deleted-file",
+	"enable-file-links-in-compare-view": "actionable-pr-view-file",
+	"use-first-commit-message-for-new-prs": "pr-first-commit-title",
+	"comment-on-draft-pr-indicator": "netiquette",
+	"draft-pr-notice": "netiquette"
+}
diff --git a/source/features/releases-tab.tsx b/source/features/releases-tab.tsx
index 9bd45856..85f74c45 100644
--- a/source/features/releases-tab.tsx
+++ b/source/features/releases-tab.tsx
@@ -91,7 +91,7 @@ async function addReleasesDropdownItem(dropdownMenu: HTMLElement): Promise<false
 
 	appendBefore(
 		dropdownMenu,
-		'.dropdown-divider', // Won't exist if `more-dropdown` is disabled
+		'.dropdown-divider', // Won't exist if `clean-repo-tabs` is disabled
 		createDropdownItem({
 			label: type,
 			href: buildRepoURL(type.toLowerCase()),
diff --git a/source/features/rgh-improve-new-issue-form.tsx b/source/features/rgh-improve-new-issue-form.tsx
index 387d2eab..d9ab723d 100644
--- a/source/features/rgh-improve-new-issue-form.tsx
+++ b/source/features/rgh-improve-new-issue-form.tsx
@@ -5,12 +5,12 @@ import * as pageDetect from 'github-url-detection';
 import features from '../feature-manager.js';
 import openOptions from '../helpers/open-options.js';
 import clearCacheHandler from '../helpers/clear-cache-handler.js';
-import {expectToken, expectTokenScope} from '../github-helpers/github-token.js';
+import {expectTokenScope, getToken} from '../github-helpers/github-token.js';
 import {isRefinedGitHubRepo} from '../github-helpers/index.js';
 
 function addNotice(adjective: JSX.Element | string): void {
 	$('#issue_body_template_name')!.before(
-		<div className="flash flash-error py-9">
+		<div className="flash flash-error h3 my-9" style={{animation: 'pulse-in 0.3s 2'}}>
 			<p>
 				Your Personal Access Token is {adjective}. Some Refined GitHub features will not work without it.
 				You can update it <button className="btn-link" type="button" onClick={openOptions as unknown as React.MouseEventHandler}>in the options</button>.
@@ -21,9 +21,7 @@ function addNotice(adjective: JSX.Element | string): void {
 }
 
 async function checkToken(): Promise<void> {
-	try {
-		await expectToken();
-	} catch {
+	if (!await getToken()) {
 		addNotice('missing');
 		return;
 	}
@@ -32,12 +30,22 @@ async function checkToken(): Promise<void> {
 		await expectTokenScope('repo');
 	} catch {
 		addNotice('invalid, expired or without enough permissions');
+		return;
 	}
+
+	// Thank you for following the instructions. I'll save you a click.
+	$('input[name^="issue_form[token]')!.checked = true;
 }
 
 async function setVersion(): Promise<void> {
 	const {version} = chrome.runtime.getManifest();
-	$('input#issue_form_version')!.value = version;
+	// Mark the submission as not having a token set up because people have a tendency to go through forms and read absolutely nothing. This makes it easier to spot liars.
+	const field = $('input#issue_form_version')!;
+	field.value = version;
+	if (!await getToken()) {
+		field.value = '(' + version + ')';
+		field.disabled = true;
+	}
 }
 
 async function linkifyCacheRefresh(): Promise<void> {
diff --git a/source/options-storage.ts b/source/options-storage.ts
index 1ebf14cc..8aea61ed 100644
--- a/source/options-storage.ts
+++ b/source/options-storage.ts
@@ -1,5 +1,6 @@
 import OptionsSyncPerDomain from 'webext-options-sync-per-domain';
 
+import renamedFeatures from './feature-renames.json';
 import {importedFeatures} from '../readme.md';
 
 export type RGHOptions = typeof defaults;
@@ -12,46 +13,6 @@ const defaults = Object.assign({
 	logHTTP: false,
 }, Object.fromEntries(importedFeatures.map(id => [`feature:${id}`, true])));
 
-const renamedFeatures = new Map<string, string>([
-	['separate-draft-pr-button', 'one-click-pr-or-gist'],
-	['prevent-pr-commit-link-loss', 'prevent-link-loss'],
-	['remove-projects-tab', 'remove-unused-repo-tabs'],
-	['remove-unused-repo-tabs', 'clean-repo-tabs'],
-	['more-dropdown', 'clean-repo-tabs'],
-	['remove-diff-signs', 'hide-diff-signs'],
-	['remove-label-faster', 'quick-label-hiding'],
-	['edit-files-faster', 'quick-file-edit'],
-	['edit-comments-faster', 'quick-comment-edit'],
-	['delete-review-comments-faster', 'quick-review-comment-deletion'],
-	['hide-comments-faster', 'quick-comment-hiding'],
-	['faster-reviews', 'quick-review'],
-	['faster-pr-diff-options', 'quick-pr-diff-options'],
-	['hide-useless-comments', 'hide-low-quality-comments'],
-	['hide-useless-newsfeed-events', 'hide-newsfeed-noise'],
-	['hide-noisy-newsfeed-events', 'hide-newsfeed-noise'],
-	['no-useless-split-diff-view', 'no-unnecessary-split-diff-view'],
-	['unwrap-useless-dropdowns', 'unwrap-unnecessary-dropdowns'],
-	['tag-changelog-link', 'tag-changes-link'],
-	['navigate-pages-with-arrow-keys', 'pagination-hotkey'],
-	['list-pr-for-branch', 'list-prs-for-branch'],
-	['quick-label-hiding', 'quick-label-removal'],
-	['next-scheduled-github-action', 'github-actions-indicators'],
-	['raw-file-link', 'more-file-links'],
-	['conversation-filters', 'more-conversation-filters'],
-	['quick-pr-diff-options', 'one-click-diff-options'],
-	['quick-review-buttons', 'one-click-review-submission'],
-	['pull-request-hotkey', 'pull-request-hotkeys'],
-	['first-published-tag-for-merged-pr', 'closing-remarks'],
-	['scheduled-and-manual-workflow-indicators', 'github-actions-indicators'],
-	['useful-forks', 'fork-notice'],
-	['set-default-repositories-type-to-sources', 'hide-user-forks'],
-	['highlight-deleted-and-added-files-in-diffs', 'new-or-deleted-file'],
-	['enable-file-links-in-compare-view', 'actionable-pr-view-file'],
-	['use-first-commit-message-for-new-prs', 'pr-first-commit-title`'],
-	['comment-on-draft-pr-indicator', 'netiquette'],
-	['draft-pr-notice', 'netiquette'],
-]);
-
 export function isFeatureDisabled(options: RGHOptions, id: string): boolean {
 	// Must check if it's specifically `false`: It could be undefined if not yet in the readme or if misread from the entry point #6606
 	// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
@@ -59,17 +20,13 @@ export function isFeatureDisabled(options: RGHOptions, id: string): boolean {
 }
 
 export function getNewFeatureName(possibleFeatureName: string): FeatureID | undefined {
-	let newFeatureName = possibleFeatureName;
-	while (renamedFeatures.has(newFeatureName)) {
-		newFeatureName = renamedFeatures.get(newFeatureName)!;
-	}
-
-	return importedFeatures.includes(newFeatureName as FeatureID) ? newFeatureName as FeatureID : undefined;
+	// @ts-expect-error Useless "no index type" error as usual
+	return renamedFeatures[possibleFeatureName];
 }
 
 const migrations = [
 	(options: RGHOptions): void => {
-		for (const [from, to] of renamedFeatures) {
+		for (const [from, to] of Object.entries(renamedFeatures)) {
 			if (typeof options[`feature:${from}`] === 'boolean') {
 				options[`feature:${to}`] = options[`feature:${from}`];
 			}
diff --git a/tsconfig.json b/tsconfig.json
index 061fb24d..ff9bd63e 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,6 +3,7 @@
 	"compilerOptions": {
 		"noUncheckedIndexedAccess": false,
 		"noPropertyAccessFromIndexSignature": false,
+		"resolveJsonModule": true,
 		"lib": [
 			"DOM",
 			"DOM.Iterable",


Test patch:
diff --git a/source/feature-renames.test.ts b/source/feature-renames.test.ts
new file mode 100644
index 00000000..ad5355af
--- /dev/null
+++ b/source/feature-renames.test.ts
@@ -0,0 +1,31 @@
+
+import {execSync} from 'node:child_process';
+import fs from 'node:fs';
+import path from 'node:path';
+import {test, expect} from 'vitest';
+
+import featureRenames from './feature-renames.json';
+
+const oldNames = Object.keys(featureRenames);
+const newNames = Object.values(featureRenames);
+
+test('Old feature names cannot appear anywhere in the repo', () => {
+	for (const oldName of oldNames) {
+		const grep = `rg -l "[^-]${oldName}[^-]" -g !feature-renames.json -g !package-lock.json`;
+		let results;
+		try {
+			results = execSync(grep, {encoding: 'utf8'});
+		} catch {
+			continue;
+		}
+
+		throw new Error(`Old feature name "${oldName}" found in ${results}`);
+	}
+});
+
+test('New feature names must exist in source/features/{name}.tsx', () => {
+	for (const newName of newNames) {
+		const filePath = path.join('source', 'features', `${newName}.tsx`);
+		expect(fs.existsSync(filePath), `New feature name "${newName}" not found in source/features`).toBe(true);
+	}
+});