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 an assert
and a deprecate
utility
#9731
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
'use strict'; | ||
|
||
/** | ||
* Verify that a certain condition is met, or throw an error if otherwise. | ||
* | ||
* This is useful for communicating expectations in the code to other human | ||
* readers as well as catching bugs that accidentally violate these expectations. | ||
* | ||
* ```js | ||
* const { assert } = require('ember-cli/lib/debug'); | ||
* | ||
* // Test for truthiness: | ||
* assert('Must pass a string.', typeof str === 'string'); | ||
* | ||
* // Fail unconditionally: | ||
* assert('This code path should never run.'); | ||
* ``` | ||
* | ||
* @method assert | ||
* @param {String} description Describes the condition. | ||
* This will become the message of the error thrown if the assertion fails. | ||
* @param {Any} condition Must be truthy for the assertion to pass. | ||
* If falsy, an error will be thrown. | ||
*/ | ||
function assert(description, condition) { | ||
if (!description) { | ||
throw new Error('When calling `assert`, you must provide a description as the first argument.'); | ||
} | ||
|
||
if (condition) { | ||
return; | ||
} | ||
|
||
throw new Error(`ASSERTION FAILED: ${description}`); | ||
} | ||
|
||
module.exports = assert; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
'use strict'; | ||
|
||
const chalk = require('chalk'); | ||
const semver = require('semver'); | ||
const assert = require('./assert'); | ||
|
||
/** | ||
* Display a deprecation message. | ||
* | ||
* ```js | ||
* const { deprecate } = require('ember-cli/lib/debug'); | ||
* | ||
* deprecate('The `foo` method is deprecated.', false, { | ||
* for: 'ember-cli', | ||
* id: 'ember-cli.foo-method', | ||
* since: { | ||
* available: '4.1.0', | ||
* enabled: '4.2.0', | ||
* }, | ||
* until: '5.0.0', | ||
* url: 'https://example.com', | ||
* }); | ||
* ``` | ||
* | ||
* @method deprecate | ||
* @param {String} description Describes the deprecation. | ||
* @param {Any} condition If falsy, the deprecation message will be displayed. | ||
* @param {Object} options An object including the deprecation's details: | ||
* - `for` The library that the deprecation is for | ||
* - `id` The deprecation's unique id | ||
* - `since.available` A SemVer version indicating when the deprecation was made available | ||
* - `since.enabled` A SemVer version indicating when the deprecation was enabled | ||
* - `until` A SemVer version indicating until when the deprecation will be active | ||
* - `url` A URL that refers to additional information about the deprecation | ||
*/ | ||
function deprecate(description, condition, options) { | ||
assert('When calling `deprecate`, you must provide a description as the first argument.', description); | ||
assert('When calling `deprecate`, you must provide a condition as the second argument.', arguments.length > 1); | ||
|
||
assert( | ||
'When calling `deprecate`, you must provide an options object as the third argument. The options object must include the `for`, `id`, `since` and `until` options (`url` is optional).', | ||
options | ||
); | ||
|
||
assert('When calling `deprecate`, you must provide the `for` option.', options.for); | ||
assert('When calling `deprecate`, you must provide the `id` option.', options.id); | ||
|
||
assert( | ||
'When calling `deprecate`, you must provide the `since` option. `since` must include the `available` and/or the `enabled` option.', | ||
options.since | ||
); | ||
|
||
assert( | ||
'When calling `deprecate`, you must provide the `since.available` and/or the `since.enabled` option.', | ||
options.since.available || options.since.enabled | ||
); | ||
|
||
assert( | ||
'`since.available` must be a valid SemVer version.', | ||
!options.since.available || isSemVer(options.since.available) | ||
); | ||
|
||
assert('`since.enabled` must be a valid SemVer version.', !options.since.enabled || isSemVer(options.since.enabled)); | ||
|
||
assert( | ||
'When calling `deprecate`, you must provide a valid SemVer version for the `until` option.', | ||
isSemVer(options.until) | ||
); | ||
|
||
if (condition) { | ||
return; | ||
} | ||
|
||
let message = formatMessage(description, options); | ||
|
||
warn(message); | ||
warn(getStackTrace()); | ||
|
||
// Return the message for testing purposes. | ||
// This can be removed once we can register deprecation handlers. | ||
return message; | ||
} | ||
|
||
function isSemVer(version) { | ||
return semver.valid(version) !== null; | ||
} | ||
|
||
function formatMessage(description, options) { | ||
let message = [`DEPRECATION: ${description}`, `[ID: ${options.id}]`]; | ||
|
||
if (options.url) { | ||
message.push(`See ${options.url} for more details.`); | ||
} | ||
|
||
return message.join(' '); | ||
} | ||
|
||
function getStackTrace() { | ||
let error = new Error(); | ||
let lines = error.stack.split('\n'); | ||
|
||
lines.shift(); // Remove the word `Error`. | ||
|
||
return lines.map((line) => line.trim()).join('\n'); | ||
} | ||
|
||
function warn(message) { | ||
console.warn(chalk.yellow(message)); | ||
} | ||
|
||
module.exports = deprecate; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
'use strict'; | ||
|
||
module.exports = { | ||
assert: require('./assert'), | ||
deprecate: require('./deprecate'), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
'use strict'; | ||
|
||
const { expect } = require('chai'); | ||
const { assert } = require('../../../lib/debug'); | ||
|
||
describe('assert', function () { | ||
it('it throws when the description argument is missing', function () { | ||
expect(() => { | ||
assert(); | ||
}).to.throw('When calling `assert`, you must provide a description as the first argument.'); | ||
|
||
expect(() => { | ||
assert(''); | ||
}).to.throw('When calling `assert`, you must provide a description as the first argument.'); | ||
}); | ||
|
||
it('it does nothing when the condition argument is truthy', function () { | ||
expect(() => { | ||
assert('description', 1); | ||
}).to.not.throw(); | ||
|
||
expect(() => { | ||
assert('description', {}); | ||
}).to.not.throw(); | ||
|
||
expect(() => { | ||
assert('description', true); | ||
}).to.not.throw(); | ||
}); | ||
|
||
it('it throws when the condition argument is falsy', function () { | ||
expect(() => { | ||
assert('description'); | ||
}).to.throw('ASSERTION FAILED: description'); | ||
|
||
expect(() => { | ||
assert('description', null); | ||
}).to.throw('ASSERTION FAILED: description'); | ||
|
||
expect(() => { | ||
assert('description', false); | ||
}).to.throw('ASSERTION FAILED: description'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
'use strict'; | ||
|
||
const { expect } = require('chai'); | ||
const { deprecate } = require('../../../lib/debug'); | ||
|
||
describe('deprecate', function () { | ||
it('it throws when the description argument is missing', function () { | ||
expect(() => { | ||
deprecate(); | ||
}).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide a description as the first argument.'); | ||
|
||
expect(() => { | ||
deprecate(''); | ||
}).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide a description as the first argument.'); | ||
}); | ||
|
||
it('it throws when the condition argument is missing', function () { | ||
expect(() => { | ||
deprecate('description'); | ||
}).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide a condition as the second argument.'); | ||
}); | ||
|
||
it('it throws when the options argument is missing', function () { | ||
expect(() => { | ||
deprecate('description', true); | ||
}).to.throw( | ||
'ASSERTION FAILED: When calling `deprecate`, you must provide an options object as the third argument. The options object must include the `for`, `id`, `since` and `until` options (`url` is optional).' | ||
); | ||
|
||
expect(() => { | ||
deprecate('description', undefined); | ||
}).to.throw( | ||
'ASSERTION FAILED: When calling `deprecate`, you must provide an options object as the third argument. The options object must include the `for`, `id`, `since` and `until` options (`url` is optional).' | ||
); | ||
|
||
expect(() => { | ||
deprecate('description', false, null); | ||
}).to.throw( | ||
'ASSERTION FAILED: When calling `deprecate`, you must provide an options object as the third argument. The options object must include the `for`, `id`, `since` and `until` options (`url` is optional).' | ||
); | ||
}); | ||
|
||
it('it throws when the `for` option is missing', function () { | ||
expect(() => { | ||
deprecate('description', true, {}); | ||
}).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide the `for` option.'); | ||
}); | ||
|
||
it('it throws when the `id` option is missing', function () { | ||
expect(() => { | ||
deprecate('description', true, { | ||
for: 'foo', | ||
}); | ||
}).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide the `id` option.'); | ||
}); | ||
|
||
it('it throws when the `since` option is missing', function () { | ||
expect(() => { | ||
deprecate('description', true, { | ||
for: 'foo', | ||
id: 'foo', | ||
}); | ||
}).to.throw( | ||
'ASSERTION FAILED: When calling `deprecate`, you must provide the `since` option. `since` must include the `available` and/or the `enabled` option.' | ||
); | ||
}); | ||
|
||
it('it throws when both the `since.available` and `since.enabled` options are missing', function () { | ||
expect(() => { | ||
deprecate('description', true, { | ||
for: 'foo', | ||
id: 'foo', | ||
since: {}, | ||
}); | ||
}).to.throw( | ||
'ASSERTION FAILED: When calling `deprecate`, you must provide the `since.available` and/or the `since.enabled` option.' | ||
); | ||
}); | ||
|
||
it('it throws when the `since.available` option is not a valid SemVer version', function () { | ||
expect(() => { | ||
deprecate('description', true, { | ||
for: 'foo', | ||
id: 'foo', | ||
since: { | ||
available: 'foo', | ||
}, | ||
}); | ||
}).to.throw('ASSERTION FAILED: `since.available` must be a valid SemVer version.'); | ||
}); | ||
|
||
it('it throws when the `since.enabled` option is not a valid SemVer version', function () { | ||
expect(() => { | ||
deprecate('description', true, { | ||
for: 'foo', | ||
id: 'foo', | ||
since: { | ||
enabled: 'foo', | ||
}, | ||
}); | ||
}).to.throw('ASSERTION FAILED: `since.enabled` must be a valid SemVer version.'); | ||
}); | ||
|
||
it('it throws when the `until` option is not a valid SemVer version', function () { | ||
expect(() => { | ||
deprecate('description', true, { | ||
for: 'foo', | ||
id: 'foo', | ||
since: { | ||
available: '4.0.0', | ||
enabled: '4.0.0', | ||
}, | ||
}); | ||
}).to.throw( | ||
'ASSERTION FAILED: When calling `deprecate`, you must provide a valid SemVer version for the `until` option.' | ||
); | ||
|
||
expect(() => { | ||
deprecate('description', true, { | ||
for: 'foo', | ||
id: 'foo', | ||
since: { | ||
available: '4.0.0', | ||
enabled: '4.0.0', | ||
}, | ||
until: 'foo', | ||
}); | ||
}).to.throw( | ||
'ASSERTION FAILED: When calling `deprecate`, you must provide a valid SemVer version for the `until` option.' | ||
); | ||
}); | ||
|
||
it('it does nothing when the condition argument is truthy', function () { | ||
let message = deprecate('description', true, { | ||
for: 'foo', | ||
id: 'foo', | ||
since: { | ||
available: '4.0.0', | ||
enabled: '4.0.0', | ||
}, | ||
until: '5.0.0', | ||
}); | ||
|
||
expect(message).to.be.undefined; | ||
}); | ||
|
||
it('it displays a deprecation message when the condition argument is falsy', function () { | ||
let message = deprecate('description', false, { | ||
for: 'foo', | ||
id: 'foo', | ||
since: { | ||
available: '4.0.0', | ||
enabled: '4.0.0', | ||
}, | ||
until: '5.0.0', | ||
}); | ||
|
||
expect(message).to.be.equal('DEPRECATION: description [ID: foo]'); | ||
}); | ||
|
||
it('it includes the `url` option in the deprecation message when provided', function () { | ||
let message = deprecate('description', false, { | ||
for: 'foo', | ||
id: 'foo', | ||
since: { | ||
available: '4.0.0', | ||
enabled: '4.0.0', | ||
}, | ||
until: '5.0.0', | ||
url: 'https://example.com', | ||
}); | ||
|
||
expect(message).to.be.equal('DEPRECATION: description [ID: foo] See https://example.com for more details.'); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
I just noticed that in
@ember/debug
we noopconsole.warn
and letdeprecate
throw during testing to do the necessary assertions. Should I do that here as well?