Enable tsc alwaysStrict, strictBindCallApply, noImplicitThis (#9600)

* Enable tsc alwaysStrict

* Enable tsc strictBindCallApply

* Enable tsc noImplicitThis

* Add d.ts

* Improve types

* Add ?

* Increase coverage

* Improve coverage
This commit is contained in:
Michael Telatynski 2022-11-21 11:24:59 +00:00 committed by GitHub
parent 0b54699829
commit 8c0d202df4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 188 additions and 68 deletions

View file

@ -212,7 +212,7 @@
"stylelint": "^14.9.1",
"stylelint-config-standard": "^26.0.0",
"stylelint-scss": "^4.2.0",
"typescript": "4.7.4",
"typescript": "4.8.4",
"walk": "^2.3.14"
},
"jest": {

View file

@ -24,15 +24,16 @@ export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
export type ReactAnyComponent = React.Component | React.ExoticComponent;
// Utility type for string dot notation for accessing nested object properties
// Based on https://stackoverflow.com/a/58436959
type Join<K, P> = K extends string | number ?
P extends string | number ?
`${K}${"" extends P ? "" : "."}${P}`
: never : never;
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]];
type Prev = [never, 0, 1, 2, 3, ...0[]];
export type Leaves<T, D extends number = 5> = [D] extends [never] ? never : T extends object ?
export type Leaves<T, D extends number = 3> = [D] extends [never] ? never : T extends object ?
{ [K in keyof T]-?: Join<K, Leaves<T[K], Prev[D]>> }[keyof T] : "";
export type RecursivePartial<T> = {

52
src/@types/commonmark.ts Normal file
View file

@ -0,0 +1,52 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as commonmark from "commonmark";
declare module "commonmark" {
export type Attr = [key: string, value: string];
export interface HtmlRenderer {
// As far as @types/commonmark is concerned, these are not public, so add them
// https://github.com/commonmark/commonmark.js/blob/master/lib/render/html.js#L272-L296
text: (this: commonmark.HtmlRenderer, node: commonmark.Node) => void;
html_inline: (this: commonmark.HtmlRenderer, node: commonmark.Node) => void;
html_block: (this: commonmark.HtmlRenderer, node: commonmark.Node) => void;
// softbreak: () => void; // This one can't be correctly specified as it is wrongly defined in @types/commonmark
linebreak: (this: commonmark.HtmlRenderer) => void;
link: (this: commonmark.HtmlRenderer, node: commonmark.Node, entering: boolean) => void;
image: (this: commonmark.HtmlRenderer, node: commonmark.Node, entering: boolean) => void;
emph: (this: commonmark.HtmlRenderer, node: commonmark.Node, entering: boolean) => void;
strong: (this: commonmark.HtmlRenderer, node: commonmark.Node, entering: boolean) => void;
paragraph: (this: commonmark.HtmlRenderer, node: commonmark.Node, entering: boolean) => void;
heading: (this: commonmark.HtmlRenderer, node: commonmark.Node, entering: boolean) => void;
code: (this: commonmark.HtmlRenderer, node: commonmark.Node) => void;
code_block: (this: commonmark.HtmlRenderer, node: commonmark.Node) => void;
thematic_break: (this: commonmark.HtmlRenderer, node: commonmark.Node) => void;
block_quote: (this: commonmark.HtmlRenderer, node: commonmark.Node, entering: boolean) => void;
list: (this: commonmark.HtmlRenderer, node: commonmark.Node, entering: boolean) => void;
item: (this: commonmark.HtmlRenderer, node: commonmark.Node, entering: boolean) => void;
custom_inline: (this: commonmark.HtmlRenderer, node: commonmark.Node, entering: boolean) => void;
custom_block: (this: commonmark.HtmlRenderer, node: commonmark.Node, entering: boolean) => void;
esc: (s: string) => string;
out: (this: commonmark.HtmlRenderer, text: string) => void;
tag: (this: commonmark.HtmlRenderer, name: string, attrs?: Attr[], selfClosing?: boolean) => void;
attrs: (this: commonmark.HtmlRenderer, node: commonmark.Node) => Attr[];
// These are inherited from the base Renderer
lit: (this: commonmark.HtmlRenderer, text: string) => void;
cr: (this: commonmark.HtmlRenderer) => void;
}
}

View file

@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import "./@types/commonmark"; // import better types than @types/commonmark
import * as commonmark from 'commonmark';
import { escape } from "lodash";
import { logger } from 'matrix-js-sdk/src/logger';
@ -26,17 +27,6 @@ const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
// These types of node are definitely text
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
// As far as @types/commonmark is concerned, these are not public, so add them
interface CommonmarkHtmlRendererInternal extends commonmark.HtmlRenderer {
paragraph: (node: commonmark.Node, entering: boolean) => void;
link: (node: commonmark.Node, entering: boolean) => void;
html_inline: (node: commonmark.Node) => void; // eslint-disable-line camelcase
html_block: (node: commonmark.Node) => void; // eslint-disable-line camelcase
text: (node: commonmark.Node) => void;
out: (text: string) => void;
emph: (node: commonmark.Node) => void;
}
function isAllowedHtmlTag(node: commonmark.Node): boolean {
if (node.literal != null &&
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|/(div|span))>$') != null) {
@ -248,7 +238,7 @@ export default class Markdown {
isPlainText(): boolean {
const walker = this.parsed.walker();
let ev;
let ev: commonmark.NodeWalkingStep;
while (ev = walker.next()) {
const node = ev.node;
if (TEXT_NODES.indexOf(node.type) > -1) {
@ -278,7 +268,7 @@ export default class Markdown {
// block quote ends up all on one line
// (https://github.com/vector-im/element-web/issues/3154)
softbreak: '<br />',
}) as CommonmarkHtmlRendererInternal;
});
// Trying to strip out the wrapping <p/> causes a lot more complication
// than it's worth, i think. For instance, this code will go and strip
@ -356,7 +346,7 @@ export default class Markdown {
* which has no formatting. Otherwise it emits HTML(!).
*/
toPlaintext(): string {
const renderer = new commonmark.HtmlRenderer({ safe: false }) as CommonmarkHtmlRendererInternal;
const renderer = new commonmark.HtmlRenderer({ safe: false });
renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
// as with toHTML, only append lines to paragraphs if there are

View file

@ -210,7 +210,7 @@ export const Notifier = {
}
},
start: function() {
start: function(this: typeof Notifier) {
// do not re-bind in the case of repeated call
this.boundOnEvent = this.boundOnEvent || this.onEvent.bind(this);
this.boundOnSyncStateChange = this.boundOnSyncStateChange || this.onSyncStateChange.bind(this);
@ -225,7 +225,7 @@ export const Notifier = {
this.isSyncing = false;
},
stop: function() {
stop: function(this: typeof Notifier) {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener(ClientEvent.Event, this.boundOnEvent);
MatrixClientPeg.get().removeListener(RoomEvent.Receipt, this.boundOnRoomReceipt);
@ -322,7 +322,7 @@ export const Notifier = {
return SettingsStore.getValue("audioNotificationsEnabled");
},
setPromptHidden: function(hidden: boolean, persistent = true) {
setPromptHidden: function(this: typeof Notifier, hidden: boolean, persistent = true) {
this.toolbarHidden = hidden;
hideNotificationsToast();
@ -343,7 +343,7 @@ export const Notifier = {
!this.isEnabled() && !this._isPromptHidden();
},
_isPromptHidden: function() {
_isPromptHidden: function(this: typeof Notifier) {
// Check localStorage for any such meta data
if (global.localStorage) {
return global.localStorage.getItem("notifications_hidden") === "true";
@ -352,7 +352,7 @@ export const Notifier = {
return this.toolbarHidden;
},
onSyncStateChange: function(state: SyncState, prevState?: SyncState, data?: ISyncStateData) {
onSyncStateChange: function(this: typeof Notifier, state: SyncState, prevState?: SyncState, data?: ISyncStateData) {
if (state === SyncState.Syncing) {
this.isSyncing = true;
} else if (state === SyncState.Stopped || state === SyncState.Error) {
@ -368,7 +368,7 @@ export const Notifier = {
}
},
onEvent: function(ev: MatrixEvent) {
onEvent: function(this: typeof Notifier, ev: MatrixEvent) {
if (!this.isSyncing) return; // don't alert for any messages initially
if (ev.getSender() === MatrixClientPeg.get().getUserId()) return;

View file

@ -111,7 +111,7 @@ export const CommandCategories = {
export type RunResult = XOR<{ error: Error | ITranslatableError }, { promise: Promise<IContent | undefined> }>;
type RunFn = ((roomId: string, args: string, cmd: string) => RunResult);
type RunFn = ((this: Command, roomId: string, args: string) => RunResult);
interface ICommandOpts {
command: string;
@ -129,9 +129,9 @@ interface ICommandOpts {
export class Command {
public readonly command: string;
public readonly aliases: string[];
public readonly args: undefined | string;
public readonly args?: string;
public readonly description: string;
public readonly runFn: undefined | RunFn;
public readonly runFn?: RunFn;
public readonly category: string;
public readonly hideCompletionAfterSpace: boolean;
public readonly renderingTypes?: TimelineRenderingType[];
@ -143,7 +143,7 @@ export class Command {
this.aliases = opts.aliases || [];
this.args = opts.args || "";
this.description = opts.description;
this.runFn = opts.runFn;
this.runFn = opts.runFn?.bind(this);
this.category = opts.category || CommandCategories.other;
this.hideCompletionAfterSpace = opts.hideCompletionAfterSpace || false;
this._isEnabled = opts.isEnabled;
@ -188,7 +188,7 @@ export class Command {
});
}
return this.runFn.bind(this)(roomId, args);
return this.runFn(roomId, args);
}
public getUsage() {
@ -1114,7 +1114,7 @@ export const Commands = [
description: _td("Sends the given message coloured as a rainbow"),
args: '<message>',
runFn: function(roomId, args) {
if (!args) return reject(this.getUserId());
if (!args) return reject(this.getUsage());
return successSync(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args)));
},
category: CommandCategories.messages,
@ -1124,7 +1124,7 @@ export const Commands = [
description: _td("Sends the given emote coloured as a rainbow"),
args: '<message>',
runFn: function(roomId, args) {
if (!args) return reject(this.getUserId());
if (!args) return reject(this.getUsage());
return successSync(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args)));
},
category: CommandCategories.messages,
@ -1207,7 +1207,7 @@ export const Commands = [
return success((async () => {
if (isPhoneNumber) {
const results = await LegacyCallHandler.instance.pstnLookup(this.state.value);
const results = await LegacyCallHandler.instance.pstnLookup(userId);
if (!results || results.length === 0 || !results[0].userid) {
throw newTranslatableError("Unable to find Matrix ID for phone number");
}

View file

@ -45,7 +45,7 @@ interface IOptions<T extends {}> {
* @param {function[]} options.funcs List of functions that when called with the
* object as an arg will return a string to use as an index
*/
export default class QueryMatcher<T extends Object> {
export default class QueryMatcher<T extends {}> {
private _options: IOptions<T>;
private _items: Map<string, {object: T, keyWeight: number}[]>;

View file

@ -47,7 +47,7 @@ export default class Draggable extends React.Component<IProps, IState> {
};
}
private onMouseDown = (event: MouseEvent): void => {
private onMouseDown = (event: React.MouseEvent): void => {
this.setState({
location: {
currentX: event.clientX,
@ -74,6 +74,6 @@ export default class Draggable extends React.Component<IProps, IState> {
}
render() {
return <div className={this.props.className} onMouseDown={this.onMouseDown.bind(this)} />;
return <div className={this.props.className} onMouseDown={this.onMouseDown} />;
}
}

View file

@ -48,7 +48,7 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I
}, () => this.updateCSSWidth(this.state.width));
}
private dragFunc = (location: ILocationState, event: React.MouseEvent<Element, MouseEvent>): ILocationState => {
private dragFunc = (location: ILocationState, event: MouseEvent): ILocationState => {
const offset = event.clientX - location.currentX;
const newWidth = this.state.width + offset;
@ -77,7 +77,7 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I
this.state.IRCLayoutRoot.style.setProperty("--name-width", newWidth + "px");
}
private onMoueUp(event: MouseEvent) {
private onMoueUp = () => {
if (this.props.roomId) {
SettingsStore.setValue(
"ircDisplayNameWidth",
@ -86,13 +86,13 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I
this.state.width,
);
}
}
};
render() {
return <Draggable
className="mx_ProfileResizer"
dragFunc={this.dragFunc.bind(this)}
onMouseUp={this.onMoueUp.bind(this)}
dragFunc={this.dragFunc}
onMouseUp={this.onMoueUp}
/>;
}
}

View file

@ -15,7 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-invalid-this */
import React from "react";
import classNames from "classnames";
@ -46,7 +45,7 @@ interface IArgs<T, D = void> {
export interface IFieldState {
value: string;
focused: boolean;
allowEmpty: boolean;
allowEmpty?: boolean;
}
export interface IValidationResult {
@ -80,10 +79,13 @@ export interface IValidationResult {
* A validation function that takes in the current input value and returns
* the overall validity and a feedback UI that can be rendered for more detail.
*/
export default function withValidation<T = undefined, D = void>({
export default function withValidation<T = void, D = void>({
description, hideDescriptionIfValid, deriveData, rules,
}: IArgs<T, D>) {
return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
return async function onValidate(
this: T,
{ value, focused, allowEmpty = true }: IFieldState,
): Promise<IValidationResult> {
if (!value && allowEmpty) {
return {
valid: null,
@ -96,7 +98,7 @@ export default function withValidation<T = undefined, D = void>({
const results: IResult[] = [];
let valid = true;
if (rules && rules.length) {
if (rules?.length) {
for (const rule of rules) {
if (!rule.key || !rule.test) {
continue;

View file

@ -31,7 +31,7 @@ function arrayBufferReadInt(arr: ArrayBuffer, start: number): number {
}
function arrayBufferReadStr(arr: ArrayBuffer, start: number, len: number): string {
return String.fromCharCode.apply(null, arrayBufferRead(arr, start, len));
return String.fromCharCode.apply(null, Array.from(arrayBufferRead(arr, start, len)));
}
export async function blobIsAnimated(mimeType: string | undefined, blob: Blob): Promise<boolean> {

View file

@ -356,7 +356,7 @@ function packMegolmKeyFile(data: Uint8Array): ArrayBuffer {
function encodeBase64(uint8Array: Uint8Array): string {
// Misinterpt the Uint8Array as Latin-1.
// window.btoa expects a unicode string with codepoints in the range 0-255.
const latin1String = String.fromCharCode.apply(null, uint8Array);
const latin1String = String.fromCharCode.apply(null, Array.from(uint8Array));
// Use the builtin base64 encoder.
return window.btoa(latin1String);
}

View file

@ -131,9 +131,9 @@ describe("ContentMessages", () => {
jest.spyOn(document, "createElement").mockImplementation(tagName => {
const element = createElement(tagName);
if (tagName === "video") {
element.load = jest.fn();
element.play = () => element.onloadeddata(new Event("loadeddata"));
element.pause = jest.fn();
(<HTMLVideoElement>element).load = jest.fn();
(<HTMLVideoElement>element).play = () => element.onloadeddata(new Event("loadeddata"));
(<HTMLVideoElement>element).pause = jest.fn();
Object.defineProperty(element, 'videoHeight', {
get() { return 600; },
});

View file

@ -433,4 +433,11 @@ describe("Notifier", () => {
expect(Notifier._displayPopupNotification).toHaveBeenCalledTimes(1);
});
});
describe("setPromptHidden", () => {
it("should persist by default", () => {
Notifier.setPromptHidden(true);
expect(localStorage.getItem("notifications_hidden")).toBeTruthy();
});
});
});

View file

@ -225,4 +225,19 @@ describe('SlashCommands', () => {
expect(client.leaveRoomChain).toHaveBeenCalledWith("room-id", expect.anything());
});
});
describe.each([
"rainbow",
"rainbowme",
])("/%s", (commandName: string) => {
const command = findCommand(commandName);
it("should return usage if no args", () => {
expect(command.run(roomId, null, null).error).toBe(command.getUsage());
});
it("should make things rainbowy", () => {
return expect(command.run(roomId, null, "this is a test message").promise).resolves.toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SlashCommands /rainbow should make things rainbowy 1`] = `
{
"body": "this is a test message",
"format": "org.matrix.custom.html",
"formatted_body": "<font color="#ff00be">t</font><font color="#ff0080">h</font><font color="#ff0041">i</font><font color="#ff5f00">s</font> <font color="#faa900">i</font><font color="#c3bf00">s</font> <font color="#00d800">a</font> <font color="#00e371">t</font><font color="#00e6b6">e</font><font color="#00e7f8">s</font><font color="#00e7ff">t</font> <font color="#00deff">m</font><font color="#00d2ff">e</font><font color="#00c0ff">s</font><font color="#44a4ff">s</font><font color="#e87dff">a</font><font color="#ff42ff">g</font><font color="#ff00fe">e</font>",
"msgtype": "m.text",
}
`;
exports[`SlashCommands /rainbowme should make things rainbowy 1`] = `
{
"body": "this is a test message",
"format": "org.matrix.custom.html",
"formatted_body": "<font color="#ff00be">t</font><font color="#ff0080">h</font><font color="#ff0041">i</font><font color="#ff5f00">s</font> <font color="#faa900">i</font><font color="#c3bf00">s</font> <font color="#00d800">a</font> <font color="#00e371">t</font><font color="#00e6b6">e</font><font color="#00e7f8">s</font><font color="#00e7ff">t</font> <font color="#00deff">m</font><font color="#00d2ff">e</font><font color="#00c0ff">s</font><font color="#44a4ff">s</font><font color="#e87dff">a</font><font color="#ff42ff">g</font><font color="#ff00fe">e</font>",
"msgtype": "m.emote",
}
`;

View file

@ -0,0 +1,31 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import withValidation from "../../../src/components/views/elements/Validation";
describe("Validation", () => {
it("should handle 0 rules", () => {
const handler = withValidation({
rules: [],
});
return expect(handler({
value: "value",
focused: true,
})).resolves.toEqual(expect.objectContaining({
valid: true,
}));
});
});

View file

@ -46,20 +46,20 @@ export const createTestPlayback = (): Playback => {
return true;
},
// EventEmitter
on: eventEmitter.on.bind(eventEmitter),
once: eventEmitter.once.bind(eventEmitter),
off: eventEmitter.off.bind(eventEmitter),
addListener: eventEmitter.addListener.bind(eventEmitter),
removeListener: eventEmitter.removeListener.bind(eventEmitter),
removeAllListeners: eventEmitter.removeAllListeners.bind(eventEmitter),
getMaxListeners: eventEmitter.getMaxListeners.bind(eventEmitter),
setMaxListeners: eventEmitter.setMaxListeners.bind(eventEmitter),
listeners: eventEmitter.listeners.bind(eventEmitter),
rawListeners: eventEmitter.rawListeners.bind(eventEmitter),
listenerCount: eventEmitter.listenerCount.bind(eventEmitter),
eventNames: eventEmitter.eventNames.bind(eventEmitter),
prependListener: eventEmitter.prependListener.bind(eventEmitter),
prependOnceListener: eventEmitter.prependOnceListener.bind(eventEmitter),
on: eventEmitter.on.bind(eventEmitter) as Playback["on"],
once: eventEmitter.once.bind(eventEmitter) as Playback["once"],
off: eventEmitter.off.bind(eventEmitter) as Playback["off"],
addListener: eventEmitter.addListener.bind(eventEmitter) as Playback["addListener"],
removeListener: eventEmitter.removeListener.bind(eventEmitter) as Playback["removeListener"],
removeAllListeners: eventEmitter.removeAllListeners.bind(eventEmitter) as Playback["removeAllListeners"],
getMaxListeners: eventEmitter.getMaxListeners.bind(eventEmitter) as Playback["getMaxListeners"],
setMaxListeners: eventEmitter.setMaxListeners.bind(eventEmitter) as Playback["setMaxListeners"],
listeners: eventEmitter.listeners.bind(eventEmitter) as Playback["listeners"],
rawListeners: eventEmitter.rawListeners.bind(eventEmitter) as Playback["rawListeners"],
listenerCount: eventEmitter.listenerCount.bind(eventEmitter) as Playback["listenerCount"],
eventNames: eventEmitter.eventNames.bind(eventEmitter) as Playback["eventNames"],
prependListener: eventEmitter.prependListener.bind(eventEmitter) as Playback["prependListener"],
prependOnceListener: eventEmitter.prependOnceListener.bind(eventEmitter) as Playback["prependOnceListener"],
liveData: new SimpleObservable<number[]>(),
durationSeconds: 31415,
timeSeconds: 3141,

View file

@ -17,7 +17,10 @@
"es2020",
"dom",
"dom.iterable"
]
],
"alwaysStrict": true,
"strictBindCallApply": true,
"noImplicitThis": true
},
"include": [
"./src/**/*.ts",

View file

@ -9451,10 +9451,10 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
typescript@4.7.4:
version "4.7.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
typescript@4.8.4:
version "4.8.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"
integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
ua-parser-js@^0.7.30:
version "0.7.31"