[JavaScript] Validate types

Lift the mists of confusion by checking that all JavaScript types align.
No ignores! This is the JavaScript equivalent to Phan.

This patch adds the necessary infrastructure for verifying typing and
fixes the few flaws found.

Bug: T239262
Change-Id: I2557471421196ea46cd13dfb786a52968fbfcc97
This commit is contained in:
Stephen Niedzielski 2020-03-09 12:31:36 -06:00
parent 7d42117f7f
commit bd7bd75569
12 changed files with 193 additions and 81 deletions

View File

@ -4,5 +4,9 @@
"wikimedia/client",
"wikimedia/jquery",
"wikimedia/mediawiki"
]
],
"rules": {
// Interferes with @type annotations.
"one-var": "off"
}
}

View File

@ -3,11 +3,11 @@ Each portal has the following composition:
string portal-id
string html-tooltip
string msg-label-id
string|null html-userlangattributes
string msg-label}
string? html-userlangattributes
string msg-label
string html-portal-content
string|null html-after-portal
string|null html-hook-vector-after-toolbox is deprecated and used by the toolbox portal.
string? html-after-portal
string? html-hook-vector-after-toolbox is deprecated and used by the toolbox portal.
}}
<div class="portal" role="navigation" id="{{portal-id}}" {{{html-tooltip}}} aria-labelledby="{{msg-label-id}}">
<h3 {{{html-userlangattributes}}} id="{{msg-label-id}}">

84
package-lock.json generated
View File

@ -1455,12 +1455,27 @@
"integrity": "sha512-iTs9HReBu7evG77Q4EC8hZnqRt57irBDkK9nvmHroiOIVwYMQc4IvYvdRgwKfYepunIY7Oh/dBuuld+Gj9uo6w==",
"dev": true
},
"@types/jquery": {
"version": "3.3.33",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.33.tgz",
"integrity": "sha512-U6IdXYGkfUI42SR79vB2Spj+h1Ly3J3UZjpd8mi943lh126TK7CB+HZOxGh2nM3IySor7wqVQdemD/xtydsBKA==",
"dev": true,
"requires": {
"@types/sizzle": "*"
}
},
"@types/minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=",
"dev": true
},
"@types/mustache": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.0.1.tgz",
"integrity": "sha512-wH6Tu9mbiOt0n5EvdoWy0VGQaJMHfLIxY/6wS0xLC7CV1taM6gESEzcYy0ZlWvxxiiljYvfDIvz4hHbUUDRlhw==",
"dev": true
},
"@types/node": {
"version": "13.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.1.tgz",
@ -1529,6 +1544,12 @@
"@types/react": "*"
}
},
"@types/sizzle": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
"dev": true
},
"@types/unist": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz",
@ -2125,8 +2146,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
"dev": true,
"optional": true
"dev": true
},
"assign-symbols": {
"version": "1.0.0",
@ -3430,7 +3450,6 @@
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"optional": true,
"requires": {
"delayed-stream": "~1.0.0"
}
@ -3938,8 +3957,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true,
"optional": true
"dev": true
},
"delegate": {
"version": "3.2.0",
@ -5148,8 +5166,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
"dev": true,
"optional": true
"dev": true
},
"fast-deep-equal": {
"version": "2.0.1",
@ -5613,8 +5630,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@ -5635,14 +5651,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -5657,20 +5671,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -5787,8 +5798,7 @@
"inherits": {
"version": "2.0.4",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -5800,7 +5810,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -5815,7 +5824,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -5823,14 +5831,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.9.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -5849,7 +5855,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -5939,8 +5944,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -5952,7 +5956,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -6038,8 +6041,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@ -6075,7 +6077,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -6095,7 +6096,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -6139,14 +6139,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},
@ -7391,8 +7389,7 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"dev": true,
"optional": true
"dev": true
},
"jsdoc": {
"version": "3.6.3",
@ -11871,8 +11868,7 @@
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"dev": true,
"optional": true
"dev": true
},
"type-check": {
"version": "0.3.2",
@ -11920,6 +11916,12 @@
"is-typedarray": "^1.0.0"
}
},
"typescript": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
"dev": true
},
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",

View File

@ -2,7 +2,7 @@
"private": true,
"scripts": {
"build": "npm -s test && npm -s run doc",
"test": "npm -s run lint",
"test": "npm -s run lint && tsc",
"lint": "npm -s run lint:js && npm -s run lint:styles && npm -s run lint:i18n",
"lint:fix": "npm -s run lint:js -- --fix && npm -s run lint:styles -- --fix",
"lint:js": "eslint --cache --max-warnings 0 .",
@ -16,6 +16,8 @@
"devDependencies": {
"@babel/core": "7.7.7",
"@storybook/html": "5.2.8",
"@types/jquery": "3.3.33",
"@types/mustache": "4.0.1",
"babel-loader": "8.0.6",
"eslint-config-wikimedia": "0.15.0",
"grunt-banana-checker": "0.8.1",
@ -26,6 +28,7 @@
"mustache": "3.0.1",
"pre-commit": "1.2.2",
"stylelint-config-wikimedia": "0.9.0",
"svgo": "1.3.2"
"svgo": "1.3.2",
"typescript": "3.8.3"
}
}

34
resources/CollapsibleTabsPlugin.d.ts vendored Normal file
View File

@ -0,0 +1,34 @@
interface JQueryStatic {
collapsibleTabs: CollapsibleTabsStatic;
}
interface JQuery {
collapsibleTabs(options: Partial<CollapsibleTabsOptions>): void;
}
/** A jQuery plugin that makes collapsible tabs for the Vector skin. */
interface CollapsibleTabsOptions {
/** Optional tab selector. Defaults to `#p-views ul`. */
expandedContainer: string;
/** Optional menu item selector. Defaults to `#p-cactions ul`. */
collapsedContainer: string;
/** Optional selector for tabs that are collapsible. Defaults to `li.collapsible`. */
collapsible: string;
shifting: boolean;
expandedWidth: number;
expandCondition(eleWidth: number): boolean;
collapseCondition(): boolean;
}
interface CollapsibleTabsStatic {
defaults: CollapsibleTabsOptions;
instances: JQuery[];
addData($collapsible: JQuery): void;
getSettings($collapsible: JQuery): CollapsibleTabsOptions;
handleResize(): void;
moveToCollapsed($moving: JQuery): void;
moveToExpanded($moving: JQuery): void;
calculateTabDistance(): number;
}
interface CollapsibleTabs extends CollapsibleTabsStatic, CollapsibleTabsOptions {}

20
resources/mediawiki.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
interface MediaWiki {
util: {
/**
* Return a wrapper function that is debounced for the given duration.
*
* When it is first called, a timeout is scheduled. If before the timer
* is reached the wrapper is called again, it gets rescheduled for the
* same duration from now until it stops being called. The original function
* is called from the "tail" of such chain, with the last set of arguments.
*
* @since 1.34
* @param {number} delay Time in milliseconds
* @param {Function} callback
* @return {Function}
*/
debounce(delay: number, callback: Function): () => void;
};
}
declare const mw: MediaWiki;

View File

@ -1,19 +1,9 @@
/** @interface CollapsibleTabsOptions */
( function () {
var boundEvent,
isRTL = document.documentElement.dir === 'rtl',
rAF = window.requestAnimationFrame || setTimeout;
/** @type {boolean|undefined} */ var boundEvent;
var isRTL = document.documentElement.dir === 'rtl';
var rAF = window.requestAnimationFrame || setTimeout;
/**
* A jQuery plugin that makes collapsible tabs for the Vector skin.
*
* @class jQuery.plugin.collapsibleTabs
* @param {Object} [options]
* @param {string} [options.expandedContainer=#p-views ul] List of tabs
* @param {string} [options.collapsedContainer=#p-cactions ul] List of menu items
* @param {string} [options.collapsible=li.collapsible] Match tabs that are collapsible
* @param {Function} [options.expandCondition]
* @param {Function} [options.collapseCondition]
*/
$.fn.collapsibleTabs = function ( options ) {
// Merge options into the defaults
var settings = $.extend( {}, $.collapsibleTabs.defaults, options );
@ -54,6 +44,7 @@
collapsedContainer: '#p-cactions ul',
collapsible: 'li.collapsible',
shifting: false,
expandedWidth: 0,
expandCondition: function ( eleWidth ) {
// If there are at least eleWidth + 1 pixels of free space, expand.
// We add 1 because .width() will truncate fractional values but .offset() will not.
@ -125,19 +116,21 @@
} );
},
moveToCollapsed: function ( $moving ) {
var outerData, expContainerSettings, target;
/** @type {CollapsibleTabsOptions} */ var outerData;
/** @type {CollapsibleTabsOptions} */ var collapsedContainerSettings;
/** @type {string} */ var target;
outerData = $.collapsibleTabs.getSettings( $moving );
if ( !outerData ) {
return;
}
expContainerSettings = $.collapsibleTabs.getSettings(
collapsedContainerSettings = $.collapsibleTabs.getSettings(
$( outerData.expandedContainer )
);
if ( !expContainerSettings ) {
if ( !collapsedContainerSettings ) {
return;
}
expContainerSettings.shifting = true;
collapsedContainerSettings.shifting = true;
// Remove the element from where it's at and put it in the dropdown menu
target = outerData.collapsedContainer;
@ -150,22 +143,26 @@
$( '<span>' ).addClass( 'placeholder' ).css( 'display', 'none' ).insertAfter( this );
$( this ).detach().prependTo( target ).data( 'collapsibleTabsSettings', outerData );
$( this ).attr( 'style', 'display: list-item;' );
expContainerSettings.shifting = false;
collapsedContainerSettings.shifting = false;
rAF( $.collapsibleTabs.handleResize );
} );
},
moveToExpanded: function ( $moving ) {
var data, expContainerSettings, $target, expandedWidth;
/** @type {CollapsibleTabsOptions} */ var data;
/** @type {CollapsibleTabsOptions} */ var expandedContainerSettings;
var $target;
var expandedWidth;
data = $.collapsibleTabs.getSettings( $moving );
if ( !data ) {
return;
}
expContainerSettings = $.collapsibleTabs.getSettings( $( data.expandedContainer ) );
if ( !expContainerSettings ) {
expandedContainerSettings =
$.collapsibleTabs.getSettings( $( data.expandedContainer ) );
if ( !expandedContainerSettings ) {
return;
}
expContainerSettings.shifting = true;
expandedContainerSettings.shifting = true;
// grab the next appearing placeholder so we can use it for replacing
$target = $( data.expandedContainer ).find( 'span.placeholder' ).first();
@ -184,9 +181,9 @@
// change the tab's contents after the page load *gasp* (T71729). This
// doesn't prevent a tab from collapsing back and forth once, but at
// least it won't continue to do that forever.
data.expandedWidth = $moving.width();
data.expandedWidth = $moving.width() || 0;
$moving.data( 'collapsibleTabsSettings', data );
expContainerSettings.shifting = false;
expandedContainerSettings.shifting = false;
$.collapsibleTabs.handleResize();
} );
} )
@ -215,10 +212,12 @@
leftTab = document.getElementById( 'right-navigation' );
rightTab = document.getElementById( 'left-navigation' );
}
leftEnd = leftTab.getBoundingClientRect().right;
rightStart = rightTab.getBoundingClientRect().left;
return rightStart - leftEnd;
if ( leftTab && rightTab ) {
leftEnd = leftTab.getBoundingClientRect().right;
rightStart = rightTab.getBoundingClientRect().left;
return rightStart - leftEnd;
}
return 0;
}
};
}() );

View File

@ -14,7 +14,7 @@ $( function () {
// must not return 0 if hidden, but rather virtually render it
// and compute its width, then hide it again. jQuery width() does
// all that for us.
var width = $cactions.width();
var width = $cactions.width() || 0;
initialCactionsWidth = function () {
return width;
};
@ -98,8 +98,8 @@ $( function () {
// 3. and, the left-navigation and right-navigation are overlapping
// each other, e.g. when making the window very narrow, or if a gadget
// added a lot of tabs.
$tabContainer.children( 'li.collapsible' ).each( function ( index, element ) {
collapsibleWidth += $( element ).width();
$tabContainer.children( 'li.collapsible' ).each( function ( _index, element ) {
collapsibleWidth += $( element ).width() || 0;
if ( collapsibleWidth > initialCactionsWidth() ) {
// We've found one or more collapsible links that are wider
// than the "More" menu would be if it were made visible,
@ -108,6 +108,7 @@ $( function () {
// Stop this possibly expensive loop the moment the condition is met once.
return false;
}
return;
} );
return doCollapse;
}

View File

@ -4,6 +4,22 @@ import '../resources/skins.vector.styles/Portal.less';
import '../.storybook/common.less';
import { placeholder, htmluserlangattributes } from './utils';
/**
* @typedef {Object} PortletContext
* @prop {string} portal-id
* @prop {string} html-tooltip
* @prop {string} msg-label-id
* @prop {string} [html-userlangattributes]
* @prop {string} msg-label
* @prop {string} html-portal-content
* @prop {string} [html-after-portal]
* @prop {string} [html-hook-vector-after-toolbox] Deprecated and used by the toolbox portal.
*/
/**
* @param {PortletContext} data
* @return {HTMLElement}
*/
export const wrapPortlet = ( data ) => {
const node = document.createElement( 'div' );
node.setAttribute( 'id', 'mw-panel' );
@ -11,6 +27,10 @@ export const wrapPortlet = ( data ) => {
return node;
};
/**
* @param {string} html
* @return {string}
*/
const portletAfter = ( html ) => {
return `<div class="after-portlet after-portlet-tb">${html}</div>`;
};
@ -33,6 +53,7 @@ export const PORTALS = {
},
navigation: {
'portal-id': 'p-navigation',
'html-tooltip': 'A message tooltip-p-navigation must exist for this to appear',
'msg-label': 'Navigation',
'msg-label-id': 'p-navigation-label',
'html-userlangattributes': htmluserlangattributes,
@ -71,7 +92,7 @@ ${placeholder( `<p>Further hook output possible (lang)</p>`, 60 )}`
},
otherProjects: {
'portal-id': 'p-wikibase-otherprojects',
'html-tooltip': 'A message tooltip-p-lang must exist for this to appear',
'html-tooltip': 'A message tooltip-p-wikibase-otherprojects must exist for this to appear',
'msg-label': 'In other projects',
'msg-label-id': 'p-wikibase-otherprojects-label',
'html-userlangattributes': htmluserlangattributes,

4
stories/rawLoader.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "!!raw-loader!*" {
const src: string;
export default src;
}

View File

@ -1,3 +1,8 @@
/**
* @param {string} msg
* @param {number} [height=200]
* @return {string}
*/
const placeholder = ( msg, height ) => {
return `<div style="width: 100%; height: ${height || 200}px; margin-bottom: 2px;
font-size: 12px; padding: 8px; box-sizing: border-box;

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"exclude": ["docs", "vendor"],
"compilerOptions": {
"resolveJsonModule": true,
"esModuleInterop": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"newLine": "lf",
"forceConsistentCasingInFileNames": true,
"pretty": true,
"target": "es5",
"lib": ["dom"],
"allowJs": true,
"checkJs": true,
"noEmit": true
}
}