-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
chore(url): add jsdoc typescript checking #17014
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,7 +30,7 @@ const newURL = addQueryArgs( 'https://google.com', { q: 'test' } ); // https://g | |
|
||
_Parameters_ | ||
|
||
- _url_ `?string`: URL to which arguments should be appended. If omitted, only the resulting querystring is returned. | ||
- _url_ `[string]`: URL to which arguments should be appended. If omitted, only the resulting querystring is returned. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find the I assume it means "optional parameter with a default value", but that only seems to vaguely align with jsdoc, where To add to the confusion, in TypeScript There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, as it stands, the way that the JSDoc is being parsed definitely results in clunky markdown for this particular case. In JSDoc, it's pretty easy to tell the difference between optional types and single-tuple types because they are written differently. // optional string type
/**
* @param {string} [foo]
*/
// single tuple string type
/**
* @param {[string]} foo
*/ So I think that you've definitely raised a valid concern with respect to how the docs are being generated. (PS: syntax comes from here) |
||
- _args_ `Object`: Query arguments to apply to URL. | ||
|
||
_Returns_ | ||
|
@@ -72,7 +72,7 @@ _Parameters_ | |
|
||
_Returns_ | ||
|
||
- `?string`: The authority part of the URL. | ||
- `(string|void)`: The authority part of the URL. | ||
|
||
<a name="getFragment" href="#getFragment">#</a> **getFragment** | ||
|
||
|
@@ -91,7 +91,7 @@ _Parameters_ | |
|
||
_Returns_ | ||
|
||
- `?string`: The fragment part of the URL. | ||
- `(string|void)`: The fragment part of the URL. | ||
|
||
<a name="getPath" href="#getPath">#</a> **getPath** | ||
|
||
|
@@ -110,7 +110,7 @@ _Parameters_ | |
|
||
_Returns_ | ||
|
||
- `?string`: The path part of the URL. | ||
- `(string|void)`: The path part of the URL. | ||
|
||
<a name="getProtocol" href="#getProtocol">#</a> **getProtocol** | ||
|
||
|
@@ -129,7 +129,7 @@ _Parameters_ | |
|
||
_Returns_ | ||
|
||
- `?string`: The protocol part of the URL. | ||
- `(string|void)`: The protocol part of the URL. | ||
|
||
<a name="getQueryArg" href="#getQueryArg">#</a> **getQueryArg** | ||
|
||
|
@@ -143,12 +143,12 @@ const foo = getQueryArg( 'https://wordpress.org?foo=bar&bar=baz', 'foo' ); // ba | |
|
||
_Parameters_ | ||
|
||
- _url_ `string`: URL | ||
- _arg_ `string`: Query arg name | ||
- _url_ `string`: URL. | ||
- _arg_ `string`: Query arg name. | ||
|
||
_Returns_ | ||
|
||
- `(Array|string)`: Query arg value. | ||
- `(QueryArgParsed|undefined)`: Query arg value. | ||
|
||
<a name="getQueryString" href="#getQueryString">#</a> **getQueryString** | ||
|
||
|
@@ -167,7 +167,7 @@ _Parameters_ | |
|
||
_Returns_ | ||
|
||
- `?string`: The query string part of the URL. | ||
- `(string|void)`: The query string part of the URL. | ||
|
||
<a name="hasQueryArg" href="#hasQueryArg">#</a> **hasQueryArg** | ||
|
||
|
@@ -181,8 +181,8 @@ const hasBar = hasQueryArg( 'https://wordpress.org?foo=bar&bar=baz', 'bar' ); // | |
|
||
_Parameters_ | ||
|
||
- _url_ `string`: URL | ||
- _arg_ `string`: Query arg name | ||
- _url_ `string`: URL. | ||
- _arg_ `string`: Query arg name. | ||
|
||
_Returns_ | ||
|
||
|
@@ -331,11 +331,11 @@ const actualURL = prependHTTP( 'wordpress.org' ); // http://wordpress.org | |
|
||
_Parameters_ | ||
|
||
- _url_ `string`: The URL to test | ||
- _url_ `string`: The URL to test. | ||
|
||
_Returns_ | ||
|
||
- `string`: The updated URL | ||
- `string`: The updated URL. | ||
|
||
<a name="removeQueryArgs" href="#removeQueryArgs">#</a> **removeQueryArgs** | ||
|
||
|
@@ -349,12 +349,12 @@ const newUrl = removeQueryArgs( 'https://wordpress.org?foo=bar&bar=baz&baz=fooba | |
|
||
_Parameters_ | ||
|
||
- _url_ `string`: URL | ||
- _args_ `...string`: Query Args | ||
- _url_ `string`: URL. | ||
- _args_ `...string`: Query Args. | ||
|
||
_Returns_ | ||
|
||
- `string`: Updated URL | ||
- `string`: Updated URL. | ||
|
||
<a name="safeDecodeURI" href="#safeDecodeURI">#</a> **safeDecodeURI** | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,14 @@ const URL_REGEXP = /^(?:https?:)?\/\/\S+$/i; | |
const EMAIL_REGEXP = /^(mailto:)?[a-z0-9._%+-]+@[a-z0-9][a-z0-9.-]*\.[a-z]{2,63}$/i; | ||
const USABLE_HREF_REGEXP = /^(?:[a-z]+:|#|\?|\.|\/)/i; | ||
|
||
/** | ||
* @typedef {{[k: string]: QueryArgParsed}} QueryArgObject | ||
dsifford marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
|
||
/** | ||
* @typedef {string|string[]|QueryArgObject} QueryArgParsed | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was added to more closely represent the possible return types of It appears that it is currently possible for the response to be an object, containing keys whose values are either This definition accounts for that. There needs to be two separate definitions because they are both cyclic and recursive. |
||
*/ | ||
|
||
/** | ||
* Determines whether the given string looks like a URL. | ||
* | ||
|
@@ -50,7 +58,7 @@ export function isEmail( email ) { | |
* const protocol2 = getProtocol( 'https://wordpress.org' ); // 'https:' | ||
* ``` | ||
* | ||
* @return {?string} The protocol part of the URL. | ||
* @return {string|void} The protocol part of the URL. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regarding the return types here in general, it's been bothering me a bit that some are It appears that As far as I can tell and for our purposes, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good observation and for all intents and purposes ....however. The TypeScript parser treats the following functions differently... // returns `void`
function foo() {
return;
}
// returns `undefined`
function bar() {
return undefined;
}
// returns `void`
function baz() {}
// returns `number | void` *see note
function qux(thing) {
if (thing) {
return 123;
}
} Actually, when writing those test cases above, I threw them at the typescript compiler just to make sure I wasn't mistaken and the last case technically returns So, for the case you point out, you're right. This should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed that in the TypeScript playground, a It's tempting to go add empty returns to the functions that are returning These are fine to leave, thanks for your thoughts 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My only worry is that we will have to update all occurrences where the return value is optional. It might be quite a huge number of lines. For what it's worth, it's at list very explicit. |
||
*/ | ||
export function getProtocol( url ) { | ||
const matches = /^([^\s:]+:)/.exec( url ); | ||
|
@@ -90,7 +98,7 @@ export function isValidProtocol( protocol ) { | |
* const authority2 = getAuthority( 'https://localhost:8080/test/' ); // 'localhost:8080' | ||
* ``` | ||
* | ||
* @return {?string} The authority part of the URL. | ||
* @return {string|void} The authority part of the URL. | ||
*/ | ||
export function getAuthority( url ) { | ||
const matches = /^[^\/\s:]+:(?:\/\/)?\/?([^\/\s#?]+)[\/#?]{0,1}\S*$/.exec( url ); | ||
|
@@ -130,7 +138,7 @@ export function isValidAuthority( authority ) { | |
* const path2 = getPath( 'https://wordpress.org/help/faq/' ); // 'help/faq' | ||
* ``` | ||
* | ||
* @return {?string} The path part of the URL. | ||
* @return {string|void} The path part of the URL. | ||
*/ | ||
export function getPath( url ) { | ||
const matches = /^[^\/\s:]+:(?:\/\/)?[^\/\s#?]+[\/]([^\s#?]+)[#?]{0,1}\S*$/.exec( url ); | ||
|
@@ -170,7 +178,7 @@ export function isValidPath( path ) { | |
* const queryString2 = getQueryString( 'https://wordpress.org#fragment?query=false&search=hello' ); // 'query=false&search=hello' | ||
* ``` | ||
* | ||
* @return {?string} The query string part of the URL. | ||
* @return {string|void} The query string part of the URL. | ||
*/ | ||
export function getQueryString( url ) { | ||
const matches = /^\S+?\?([^\s#]+)/.exec( url ); | ||
|
@@ -210,7 +218,7 @@ export function isValidQueryString( queryString ) { | |
* const fragment2 = getFragment( 'https://wordpress.org#another-fragment?query=true' ); // '#another-fragment' | ||
* ``` | ||
* | ||
* @return {?string} The fragment part of the URL. | ||
* @return {string|void} The fragment part of the URL. | ||
*/ | ||
export function getFragment( url ) { | ||
const matches = /^\S+?(#[^\s\?]*)/.exec( url ); | ||
|
@@ -244,9 +252,9 @@ export function isValidFragment( fragment ) { | |
* includes query arguments, the arguments are merged with (and take precedent | ||
* over) the existing set. | ||
* | ||
* @param {?string} url URL to which arguments should be appended. If omitted, | ||
* only the resulting querystring is returned. | ||
* @param {Object} args Query arguments to apply to URL. | ||
* @param {string} [url=''] URL to which arguments should be appended. If omitted, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The default value seems like a duplication for me, can't it be infered? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is mostly important for documentation generation. Otherwise, I'm mainly just following the WordPress JSDoc standards posted here. To answer your question: Yes, the TypeScript compiler can infer this. |
||
* only the resulting querystring is returned. | ||
* @param {Object} args Query arguments to apply to URL. | ||
* | ||
* @example | ||
* ```js | ||
|
@@ -282,15 +290,15 @@ export function addQueryArgs( url = '', args ) { | |
/** | ||
* Returns a single query argument of the url | ||
* | ||
* @param {string} url URL | ||
* @param {string} arg Query arg name | ||
* @param {string} url URL. | ||
* @param {string} arg Query arg name. | ||
* | ||
* @example | ||
* ```js | ||
* const foo = getQueryArg( 'https://wordpress.org?foo=bar&bar=baz', 'foo' ); // bar | ||
* ``` | ||
* | ||
* @return {Array|string} Query arg value. | ||
* @return {QueryArgParsed|undefined} Query arg value. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought we might leverage function parse(str: string, options?: IParseOptions): any; That's not very helpful, I suspect it's very difficult to type the return because it can vary based on the input and options. We control the options and can likely provide better type information as you've done. |
||
*/ | ||
export function getQueryArg( url, arg ) { | ||
const queryStringIndex = url.indexOf( '?' ); | ||
|
@@ -302,8 +310,8 @@ export function getQueryArg( url, arg ) { | |
/** | ||
* Determines whether the URL contains a given query arg. | ||
* | ||
* @param {string} url URL | ||
* @param {string} arg Query arg name | ||
* @param {string} url URL. | ||
* @param {string} arg Query arg name. | ||
* | ||
* @example | ||
* ```js | ||
|
@@ -319,15 +327,15 @@ export function hasQueryArg( url, arg ) { | |
/** | ||
* Removes arguments from the query string of the url | ||
* | ||
* @param {string} url URL | ||
* @param {...string} args Query Args | ||
* @param {string} url URL. | ||
* @param {...string} args Query Args. | ||
* | ||
* @example | ||
* ```js | ||
* const newUrl = removeQueryArgs( 'https://wordpress.org?foo=bar&bar=baz&baz=foobar', 'foo', 'bar' ); // https://wordpress.org?baz=foobar | ||
* ``` | ||
* | ||
* @return {string} Updated URL | ||
* @return {string} Updated URL. | ||
*/ | ||
export function removeQueryArgs( url, ...args ) { | ||
const queryStringIndex = url.indexOf( '?' ); | ||
|
@@ -342,14 +350,14 @@ export function removeQueryArgs( url, ...args ) { | |
/** | ||
* Prepends "http://" to a url, if it looks like something that is meant to be a TLD. | ||
* | ||
* @param {string} url The URL to test | ||
* @param {string} url The URL to test. | ||
* | ||
* @example | ||
* ```js | ||
* const actualURL = prependHTTP( 'wordpress.org' ); // http://wordpress.org | ||
* ``` | ||
* | ||
* @return {string} The updated URL | ||
* @return {string} The updated URL. | ||
*/ | ||
export function prependHTTP( url ) { | ||
if ( ! USABLE_HREF_REGEXP.test( url ) && ! EMAIL_REGEXP.test( url ) ) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
{ | ||
"compilerOptions": { | ||
"allowJs": true, | ||
"allowSyntheticDefaultImports": true, | ||
"checkJs": true, | ||
"jsx": "preserve", | ||
"lib": ["dom", "esnext"], | ||
"module": "commonjs", | ||
"noEmit": true, | ||
"resolveJsonModule": true, | ||
"target": "esnext", | ||
|
||
/* Strict Type-Checking Options */ | ||
"strict": true, /* Enable all strict type-checking options. */ | ||
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we allow implicit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can decide to get stricter in the future, but adding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried disabling this and the only error was missing types for I'm on the fence because it seems much easier to relax things when you're too strict than to become more strict when you've been too relaxed 🙂 Have you already investigated parts of the code where strictness here becomes overly cumbersome and could you provide an example? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have not. But I'm nervous that we'd lose people who bought into this crazy idea if I told them we'd have to depend on more external types. No specific examples I can think of off the top of my head but, as you've already alluded, the issues that arise will be almost (if not entirely) exclusive to missing type definitions for externals. We could fix that if we pull in those defs from DefinitelyTyped, however there was some concern already about pulling in the types for this repo from DefinitelyTyped so not sure how the response to that ask would go over (see side note). I'm all for making it as strict as possible.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Good point 👍 |
||
|
||
/* Additional Checks */ | ||
"noUnusedLocals": true, /* Report errors on unused locals. */ | ||
"noUnusedParameters": true, /* Report errors on unused parameters. */ | ||
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ | ||
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ | ||
|
||
}, | ||
"include": [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, let's skip tests for now and revisit our approach once we have all packages enabled here 👍 |
||
"./packages/url/**/*.js" | ||
], | ||
"exclude": [ | ||
"./packages/**/test/**", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At some point, you will have to include:
Just saying, there are a few packages which use them :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. I'll add it in when the issue arises if that's okay with you. |
||
"./packages/**/build/**", | ||
"./packages/**/build-module/**" | ||
] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we install the typescript dev dependency? I think travis is failing because of it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops, sorry about that. On it now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we include this command in
lint-staged
section of this file to have it executed as a pre-commit hook?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if we can lint only a single file at a time because type compilation generally requires knowledge of the entire project.
Also, the compiler seems to freak out if you feed it files with a
.js
extension by itself (regardless of whether or not they are allowed)...So, in short, we can add it it the lint-staged flow, but it technically won't be able to lint just the staged file(s). Up to you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can skip it for now if it produces issues, no worries. As long as Travis is going to catch those violations, we are good 👍